| use std::path::Path; |
| use std::time::{Duration, Instant}; |
| |
| use futures::future::Future; |
| use lsp_types::{notification::*, request::*, *}; |
| use serde::de::Deserialize; |
| use serde_json::json; |
| |
| use crate::support::project_builder::{project, ProjectBuilder}; |
| use crate::support::{basic_bin_manifest, fixtures_dir}; |
| |
| #[allow(dead_code)] |
| mod support; |
| |
| fn initialize_params(root_path: &Path) -> InitializeParams { |
| InitializeParams { |
| process_id: None, |
| root_uri: None, |
| root_path: Some(root_path.display().to_string()), |
| initialization_options: None, |
| capabilities: ClientCapabilities { |
| workspace: None, |
| window: Some(WindowClientCapabilities { progress: Some(true) }), |
| text_document: None, |
| experimental: None, |
| }, |
| trace: None, |
| workspace_folders: None, |
| } |
| } |
| |
| fn initialize_params_with_opts(root_path: &Path, opts: serde_json::Value) -> InitializeParams { |
| InitializeParams { initialization_options: Some(opts), ..initialize_params(root_path) } |
| } |
| |
| #[test] |
| fn client_test_infer_bin() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("infer_bin")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("src/main.rs")); |
| assert!(diag.diagnostics[0].message.contains("struct is never constructed: `UnusedBin`")); |
| } |
| |
| #[test] |
| fn client_test_infer_lib() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("infer_lib")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("src/lib.rs")); |
| assert!(diag.diagnostics[0].message.contains("struct is never constructed: `UnusedLib`")); |
| } |
| |
| #[test] |
| fn client_test_infer_custom_bin() { |
| let p = |
| ProjectBuilder::try_from_fixture(fixtures_dir().join("infer_custom_bin")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("src/custom_bin.rs")); |
| assert!(diag.diagnostics[0].message.contains("struct is never constructed: `UnusedCustomBin`")); |
| } |
| |
| /// Test includes window/progress regression testing |
| #[test] |
| fn client_test_simple_workspace() { |
| let p = project("simple_workspace") |
| .file( |
| "Cargo.toml", |
| r#" |
| [workspace] |
| members = [ |
| "member_lib", |
| "member_bin", |
| ] |
| "#, |
| ) |
| .file( |
| "Cargo.lock", |
| r#" |
| [root] |
| name = "member_lib" |
| version = "0.1.0" |
| |
| [[package]] |
| name = "member_bin" |
| version = "0.1.0" |
| dependencies = [ |
| "member_lib 0.1.0", |
| ] |
| "#, |
| ) |
| .file( |
| "member_bin/Cargo.toml", |
| r#" |
| [package] |
| name = "member_bin" |
| version = "0.1.0" |
| authors = ["Igor Matuszewski <Xanewok@gmail.com>"] |
| |
| [dependencies] |
| member_lib = { path = "../member_lib" } |
| "#, |
| ) |
| .file( |
| "member_bin/src/main.rs", |
| r#" |
| extern crate member_lib; |
| |
| fn main() { |
| let a = member_lib::MemberLibStruct; |
| } |
| "#, |
| ) |
| .file( |
| "member_lib/Cargo.toml", |
| r#" |
| [package] |
| name = "member_lib" |
| version = "0.1.0" |
| authors = ["Igor Matuszewski <Xanewok@gmail.com>"] |
| |
| [dependencies] |
| "#, |
| ) |
| .file( |
| "member_lib/src/lib.rs", |
| r#" |
| pub struct MemberLibStruct; |
| |
| struct Unused; |
| |
| #[cfg(test)] |
| mod tests { |
| #[test] |
| fn it_works() { |
| } |
| } |
| "#, |
| ) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| // Check if we built member_lib and member_bin + their cfg(test) variants |
| let count = rls |
| .messages() |
| .iter() |
| .filter(|msg| msg["method"] == "window/progress") |
| .filter(|msg| msg["params"]["title"] == "Building") |
| .filter(|msg| { |
| msg["params"]["message"].as_str().map(|x| x.starts_with("member_")).unwrap_or(false) |
| }) |
| .count(); |
| assert_eq!(count, 4); |
| } |
| |
| #[test] |
| fn client_changing_workspace_lib_retains_diagnostics() { |
| let p = project("simple_workspace") |
| .file( |
| "Cargo.toml", |
| r#" |
| [workspace] |
| members = [ |
| "library", |
| "binary", |
| ] |
| "#, |
| ) |
| .file( |
| "library/Cargo.toml", |
| r#" |
| [package] |
| name = "library" |
| version = "0.1.0" |
| authors = ["Example <rls@example.com>"] |
| "#, |
| ) |
| .file( |
| "library/src/lib.rs", |
| r#" |
| pub fn fetch_u32() -> u32 { |
| let unused = (); |
| 42 |
| } |
| #[cfg(test)] |
| mod test { |
| #[test] |
| fn my_test() { |
| let test_val: u32 = super::fetch_u32(); |
| } |
| } |
| "#, |
| ) |
| .file( |
| "binary/Cargo.toml", |
| r#" |
| [package] |
| name = "binary" |
| version = "0.1.0" |
| authors = ["Igor Matuszewski <Xanewok@gmail.com>"] |
| |
| [dependencies] |
| library = { path = "../library" } |
| "#, |
| ) |
| .file( |
| "binary/src/main.rs", |
| r#" |
| extern crate library; |
| |
| fn main() { |
| let val: u32 = library::fetch_u32(); |
| } |
| "#, |
| ) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let lib = rls.future_diagnostics("library/src/lib.rs"); |
| let bin = rls.future_diagnostics("binary/src/main.rs"); |
| let (lib, bin) = rls.block_on(lib.join(bin)).unwrap(); |
| |
| assert!(lib.diagnostics.iter().any(|m| m.message.contains("unused variable: `test_val`"))); |
| assert!(lib.diagnostics.iter().any(|m| m.message.contains("unused variable: `unused`"))); |
| assert!(bin.diagnostics[0].message.contains("unused variable: `val`")); |
| |
| rls.notify::<DidChangeTextDocument>(DidChangeTextDocumentParams { |
| content_changes: vec![TextDocumentContentChangeEvent { |
| range: Some(Range { |
| start: Position { line: 1, character: 38 }, |
| end: Position { line: 1, character: 41 }, |
| }), |
| range_length: Some(3), |
| text: "u64".to_string(), |
| }], |
| text_document: VersionedTextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("library/src/lib.rs")).unwrap(), |
| version: Some(0), |
| }, |
| }); |
| |
| let lib = rls.future_diagnostics("library/src/lib.rs"); |
| let bin = rls.future_diagnostics("binary/src/main.rs"); |
| let (lib, bin) = rls.block_on(lib.join(bin)).unwrap(); |
| |
| // lib unit tests have compile errors |
| assert!(lib.diagnostics.iter().any(|m| m.message.contains("unused variable: `unused`"))); |
| assert!(lib.diagnostics.iter().any(|m| m.message.contains("expected u32, found u64"))); |
| // bin depending on lib picks up type mismatch |
| assert!(bin.diagnostics[0].message.contains("mismatched types\n\nexpected u32, found u64")); |
| |
| rls.notify::<DidChangeTextDocument>(DidChangeTextDocumentParams { |
| content_changes: vec![TextDocumentContentChangeEvent { |
| range: Some(Range { |
| start: Position { line: 1, character: 38 }, |
| end: Position { line: 1, character: 41 }, |
| }), |
| range_length: Some(3), |
| text: "u32".to_string(), |
| }], |
| text_document: VersionedTextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("library/src/lib.rs")).unwrap(), |
| version: Some(1), |
| }, |
| }); |
| |
| let lib = rls.future_diagnostics("library/src/lib.rs"); |
| let bin = rls.future_diagnostics("binary/src/main.rs"); |
| let (lib, bin) = rls.block_on(lib.join(bin)).unwrap(); |
| |
| assert!(lib.diagnostics.iter().any(|m| m.message.contains("unused variable: `test_val`"))); |
| assert!(lib.diagnostics.iter().any(|m| m.message.contains("unused variable: `unused`"))); |
| assert!(bin.diagnostics[0].message.contains("unused variable: `val`")); |
| } |
| |
| #[test] |
| fn client_implicit_workspace_pick_up_lib_changes() { |
| let p = project("simple_workspace") |
| .file( |
| "Cargo.toml", |
| r#" |
| [package] |
| name = "binary" |
| version = "0.1.0" |
| authors = ["Example <rls@example.com>"] |
| |
| [dependencies] |
| inner = { path = "inner" } |
| "#, |
| ) |
| .file( |
| "src/main.rs", |
| r#" |
| extern crate inner; |
| |
| fn main() { |
| let val = inner::foo(); |
| } |
| "#, |
| ) |
| .file( |
| "inner/Cargo.toml", |
| r#" |
| [package] |
| name = "inner" |
| version = "0.1.0" |
| authors = ["Example <rls@example.com>"] |
| "#, |
| ) |
| .file( |
| "inner/src/lib.rs", |
| r#" |
| pub fn foo() -> u32 { 42 } |
| "#, |
| ) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let bin = rls.future_diagnostics("src/main.rs"); |
| let bin = rls.block_on(bin).unwrap(); |
| assert!(bin.diagnostics[0].message.contains("unused variable: `val`")); |
| |
| rls.notify::<DidChangeTextDocument>(DidChangeTextDocumentParams { |
| content_changes: vec![TextDocumentContentChangeEvent { |
| range: Some(Range { |
| start: Position { line: 1, character: 23 }, |
| end: Position { line: 1, character: 26 }, |
| }), |
| range_length: Some(3), |
| text: "bar".to_string(), |
| }], |
| text_document: VersionedTextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("inner/src/lib.rs")).unwrap(), |
| version: Some(0), |
| }, |
| }); |
| |
| // bin depending on lib picks up type mismatch |
| let bin = rls.future_diagnostics("src/main.rs"); |
| let bin = rls.block_on(bin).unwrap(); |
| assert!(bin.diagnostics[0].message.contains("cannot find function `foo`")); |
| |
| rls.notify::<DidChangeTextDocument>(DidChangeTextDocumentParams { |
| content_changes: vec![TextDocumentContentChangeEvent { |
| range: Some(Range { |
| start: Position { line: 1, character: 23 }, |
| end: Position { line: 1, character: 26 }, |
| }), |
| range_length: Some(3), |
| text: "foo".to_string(), |
| }], |
| text_document: VersionedTextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("inner/src/lib.rs")).unwrap(), |
| version: Some(1), |
| }, |
| }); |
| |
| let bin = rls.future_diagnostics("src/main.rs"); |
| let bin = rls.block_on(bin).unwrap(); |
| assert!(bin.diagnostics[0].message.contains("unused variable: `val`")); |
| } |
| |
| #[test] |
| fn client_test_complete_self_crate_name() { |
| let p = project("ws_with_test_dir") |
| .file( |
| "Cargo.toml", |
| r#" |
| [workspace] |
| members = ["library"] |
| "#, |
| ) |
| .file( |
| "library/Cargo.toml", |
| r#" |
| [package] |
| name = "library" |
| version = "0.1.0" |
| authors = ["Example <rls@example.com>"] |
| "#, |
| ) |
| .file( |
| "library/src/lib.rs", |
| r#" |
| pub fn function() -> usize { 5 } |
| "#, |
| ) |
| .file( |
| "library/tests/test.rs", |
| r#" |
| extern crate library; |
| use library::~ |
| "#, |
| ) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| assert!(diag.diagnostics[0].message.contains("expected identifier")); |
| |
| let response = rls.request::<Completion>( |
| 100, |
| CompletionParams { |
| context: Some(CompletionContext { |
| trigger_character: Some(":".to_string()), |
| trigger_kind: CompletionTriggerKind::TriggerCharacter, |
| }), |
| position: Position::new(2, 32), |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("library/tests/test.rs")).unwrap(), |
| }, |
| }, |
| ); |
| |
| let items = match response { |
| Some(CompletionResponse::Array(items)) => items, |
| Some(CompletionResponse::List(CompletionList { items, .. })) => items, |
| _ => Vec::new(), |
| }; |
| |
| let item = items.into_iter().nth(0).expect("Racer autocompletion failed"); |
| assert_eq!(item.detail.unwrap(), "pub fn function() -> usize"); |
| } |
| |
| // Spurious in Rust CI, e.g. |
| // https://github.com/rust-lang/rust/pull/60730 |
| // https://github.com/rust-lang/rust/pull/61771 |
| // https://github.com/rust-lang/rust/pull/61932 |
| #[ignore] |
| #[test] |
| fn client_completion_suggests_arguments_in_statements() { |
| let p = project("ws_with_test_dir") |
| .file( |
| "Cargo.toml", |
| r#" |
| [workspace] |
| members = ["library"] |
| "#, |
| ) |
| .file( |
| "library/Cargo.toml", |
| r#" |
| [package] |
| name = "library" |
| version = "0.1.0" |
| authors = ["Example <rls@example.com>"] |
| "#, |
| ) |
| .file( |
| "library/src/lib.rs", |
| r#" |
| pub fn function() -> usize { 5 } |
| "#, |
| ) |
| .file( |
| "library/tests/test.rs", |
| r#" |
| extern crate library; |
| fn magic() { |
| let a = library::f~ |
| } |
| "#, |
| ) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>( |
| 0, |
| lsp_types::InitializeParams { |
| process_id: None, |
| root_uri: None, |
| root_path: Some(root_path.display().to_string()), |
| initialization_options: None, |
| capabilities: lsp_types::ClientCapabilities { |
| workspace: None, |
| window: Some(WindowClientCapabilities { progress: Some(true) }), |
| text_document: Some(TextDocumentClientCapabilities { |
| completion: Some(CompletionCapability { |
| completion_item: Some(CompletionItemCapability { |
| snippet_support: Some(true), |
| ..CompletionItemCapability::default() |
| }), |
| ..CompletionCapability::default() |
| }), |
| ..TextDocumentClientCapabilities::default() |
| }), |
| experimental: None, |
| }, |
| trace: None, |
| workspace_folders: None, |
| }, |
| ); |
| |
| let diag = rls.wait_for_diagnostics(); |
| assert!(diag.diagnostics[0].message.contains("expected one of")); |
| |
| let response = rls.request::<Completion>( |
| 100, |
| CompletionParams { |
| context: Some(CompletionContext { |
| trigger_character: Some("f".to_string()), |
| trigger_kind: CompletionTriggerKind::TriggerCharacter, |
| }), |
| position: Position::new(3, 41), |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("library/tests/test.rs")).unwrap(), |
| }, |
| }, |
| ); |
| |
| let items = match response { |
| Some(CompletionResponse::Array(items)) => items, |
| Some(CompletionResponse::List(CompletionList { items, .. })) => items, |
| _ => Vec::new(), |
| }; |
| |
| let item = items.into_iter().nth(0).expect("Racer autocompletion failed"); |
| assert_eq!(item.insert_text.unwrap(), "function()"); |
| } |
| |
| #[test] |
| fn client_use_statement_completion_doesnt_suggest_arguments() { |
| let p = project("ws_with_test_dir") |
| .file( |
| "Cargo.toml", |
| r#" |
| [workspace] |
| members = ["library"] |
| "#, |
| ) |
| .file( |
| "library/Cargo.toml", |
| r#" |
| [package] |
| name = "library" |
| version = "0.1.0" |
| authors = ["Example <rls@example.com>"] |
| "#, |
| ) |
| .file( |
| "library/src/lib.rs", |
| r#" |
| pub fn function() -> usize { 5 } |
| "#, |
| ) |
| .file( |
| "library/tests/test.rs", |
| r#" |
| extern crate library; |
| use library::~; |
| "#, |
| ) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| assert!(diag.diagnostics[0].message.contains("expected identifier")); |
| |
| let response = rls.request::<Completion>( |
| 100, |
| CompletionParams { |
| context: Some(CompletionContext { |
| trigger_character: Some(":".to_string()), |
| trigger_kind: CompletionTriggerKind::TriggerCharacter, |
| }), |
| position: Position::new(2, 32), |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("library/tests/test.rs")).unwrap(), |
| }, |
| }, |
| ); |
| |
| let items = match response { |
| Some(CompletionResponse::Array(items)) => items, |
| Some(CompletionResponse::List(CompletionList { items, .. })) => items, |
| _ => Vec::new(), |
| }; |
| |
| let item = items.into_iter().nth(0).expect("Racer autocompletion failed"); |
| assert_eq!(item.insert_text.unwrap(), "function"); |
| } |
| |
| /// Test simulates typing in a dependency wrongly in a couple of ways before finally getting it |
| /// right. Rls should provide Cargo.toml diagnostics. |
| /// |
| /// ``` |
| /// [dependencies] |
| /// version-check = "0.5555" |
| /// ``` |
| /// |
| /// * Firstly "version-check" doesn't exist, it should be "version_check" |
| /// * Secondly version 0.5555 of "version_check" doesn't exist. |
| #[test] |
| fn client_dependency_typo_and_fix() { |
| let manifest_with_dependency = |dep: &str| { |
| format!( |
| r#" |
| [package] |
| name = "dependency_typo" |
| version = "0.1.0" |
| authors = ["alexheretic@gmail.com"] |
| |
| [dependencies] |
| {} |
| "#, |
| dep |
| ) |
| }; |
| |
| let p = project("dependency_typo") |
| .file("Cargo.toml", &manifest_with_dependency(r#"version-check = "0.5555""#)) |
| .file( |
| "src/main.rs", |
| r#" |
| fn main() { |
| println!("Hello world!"); |
| } |
| "#, |
| ) |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| assert!(diag.diagnostics[0].message.contains("no matching package named `version-check`")); |
| |
| let change_manifest = |contents: &str| { |
| std::fs::write(root_path.join("Cargo.toml"), contents).unwrap(); |
| }; |
| |
| // fix naming typo, we now expect a version error diagnostic |
| change_manifest(&manifest_with_dependency(r#"version_check = "0.5555""#)); |
| rls.notify::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams { |
| changes: vec![FileEvent { |
| uri: Url::from_file_path(p.root().join("Cargo.toml")).unwrap(), |
| typ: FileChangeType::Changed, |
| }], |
| }); |
| |
| let diag = rls.wait_for_diagnostics(); |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| assert!(diag.diagnostics[0].message.contains("^0.5555")); |
| |
| // Fix version issue so no error diagnostics occur. |
| // This is kinda slow as cargo will compile the dependency, though I |
| // chose version_check to minimise this as it is a very small dependency. |
| change_manifest(&manifest_with_dependency(r#"version_check = "0.1""#)); |
| rls.notify::<DidChangeWatchedFiles>(DidChangeWatchedFilesParams { |
| changes: vec![FileEvent { |
| uri: Url::from_file_path(p.root().join("Cargo.toml")).unwrap(), |
| typ: FileChangeType::Changed, |
| }], |
| }); |
| |
| let diag = rls.wait_for_diagnostics(); |
| assert_eq!( |
| diag.diagnostics.iter().find(|d| d.severity == Some(DiagnosticSeverity::Error)), |
| None |
| ); |
| } |
| |
| /// Tests correct positioning of a toml parse error, use of `==` instead of `=`. |
| #[test] |
| fn client_invalid_toml_manifest() { |
| let p = project("invalid_toml") |
| .file( |
| "Cargo.toml", |
| r#"[package] |
| name = "probably_valid" |
| version == "0.1.0" |
| authors = ["alexheretic@gmail.com"] |
| "#, |
| ) |
| .file( |
| "src/main.rs", |
| r#" |
| fn main() { |
| println!("Hello world!"); |
| } |
| "#, |
| ) |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag: PublishDiagnosticsParams = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("invalid_toml/Cargo.toml")); |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| assert!(diag.diagnostics[0].message.contains("failed to parse manifest")); |
| |
| assert_eq!( |
| diag.diagnostics[0].range, |
| Range { |
| start: Position { line: 2, character: 21 }, |
| end: Position { line: 2, character: 22 }, |
| } |
| ); |
| } |
| |
| /// Tests correct file highlighting of workspace member manifest with invalid path dependency. |
| #[test] |
| fn client_invalid_member_toml_manifest() { |
| let project = project("invalid_member_toml") |
| .file( |
| "Cargo.toml", |
| r#"[package] |
| name = "root_is_fine" |
| version = "0.1.0" |
| authors = ["alexheretic@gmail.com"] |
| |
| [dependencies] |
| member_a = { path = "member_a" } |
| |
| [workspace] |
| "#, |
| ) |
| .file("src/main.rs", "fn main() {}") |
| .file( |
| "member_a/Cargo.toml", |
| r#"[package] |
| name = "member_a" |
| version = "0.0.3" |
| authors = ["alexheretic@gmail.com"] |
| |
| [dependencies] |
| dodgy_member = { path = "dodgy_member" } |
| "#, |
| ) |
| .file("member_a/src/lib.rs", "fn ma() {}") |
| .file( |
| "member_a/dodgy_member/Cargo.toml", |
| r#"[package] |
| name = "dodgy_member" |
| version = "0.5.0" |
| authors = ["alexheretic@gmail.com"] |
| |
| [dependencies] |
| nosuch = { path = "not-exist" } |
| "#, |
| ) |
| .file("member_a/dodgy_member/src/lib.rs", "fn dm() {}") |
| .build(); |
| let root_path = project.root(); |
| let mut rls = project.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag: PublishDiagnosticsParams = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("invalid_member_toml/member_a/dodgy_member/Cargo.toml")); |
| |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| assert!(diag.diagnostics[0].message.contains("failed to read")); |
| } |
| |
| #[test] |
| fn client_invalid_member_dependency_resolution() { |
| let project = project("invalid_member_resolution") |
| .file( |
| "Cargo.toml", |
| r#"[package] |
| name = "root_is_fine" |
| version = "0.1.0" |
| authors = ["alexheretic@gmail.com"] |
| |
| [dependencies] |
| member_a = { path = "member_a" } |
| |
| [workspace] |
| "#, |
| ) |
| .file("src/main.rs", "fn main() {}") |
| .file( |
| "member_a/Cargo.toml", |
| r#"[package] |
| name = "member_a" |
| version = "0.0.5" |
| authors = ["alexheretic@gmail.com"] |
| |
| [dependencies] |
| dodgy_member = { path = "dodgy_member" } |
| "#, |
| ) |
| .file("member_a/src/lib.rs", "fn ma() {}") |
| .file( |
| "member_a/dodgy_member/Cargo.toml", |
| r#"[package] |
| name = "dodgy_member" |
| version = "0.6.0" |
| authors = ["alexheretic@gmail.com"] |
| |
| [dependencies] |
| nosuchdep123 = "1.2.4" |
| "#, |
| ) |
| .file("member_a/dodgy_member/src/lib.rs", "fn dm() {}") |
| .build(); |
| let root_path = project.root(); |
| let mut rls = project.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag: PublishDiagnosticsParams = rls.wait_for_diagnostics(); |
| |
| assert!(diag |
| .uri |
| .as_str() |
| .ends_with("invalid_member_resolution/member_a/dodgy_member/Cargo.toml")); |
| |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| assert!(diag.diagnostics[0].message.contains("no matching package named `nosuchdep123`")); |
| } |
| |
| #[test] |
| fn client_handle_utf16_unit_text_edits() { |
| let p = project("client_handle_utf16_unit_text_edits") |
| .file( |
| "Cargo.toml", |
| r#"[package] |
| name = "client_handle_utf16_unit_text_edits" |
| version = "0.1.0" |
| authors = ["example@example.com"] |
| "#, |
| ) |
| .file("src/main.rs", "fn main() {}") |
| .file("src/some.rs", "😢") |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| rls.notify::<DidChangeTextDocument>(DidChangeTextDocumentParams { |
| text_document: VersionedTextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/some.rs")).unwrap(), |
| version: Some(0), |
| }, |
| // "😢" -> "" |
| content_changes: vec![TextDocumentContentChangeEvent { |
| range: Some(Range { |
| start: Position { line: 0, character: 0 }, |
| end: Position { line: 0, character: 2 }, |
| }), |
| range_length: Some(2), |
| text: "".to_string(), |
| }], |
| }); |
| } |
| |
| /// Ensures that wide characters do not prevent RLS from calculating correct |
| /// 'whole file' LSP range. |
| #[test] |
| fn client_format_utf16_range() { |
| let p = project("client_format_utf16_range") |
| .file( |
| "Cargo.toml", |
| r#"[package] |
| name = "client_format_utf16_range" |
| version = "0.1.0" |
| authors = ["example@example.com"] |
| "#, |
| ) |
| .file("src/main.rs", "/* 😢😢😢😢😢😢😢 */ fn main() { }") |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls.request::<Formatting>( |
| 66, |
| DocumentFormattingParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| options: FormattingOptions { |
| tab_size: 4, |
| insert_spaces: true, |
| properties: Default::default(), |
| }, |
| }, |
| ); |
| |
| let new_text: Vec<_> = |
| result.unwrap().iter().map(|edit| edit.new_text.as_str().replace('\r', "")).collect(); |
| // Actual formatting isn't important - what is, is that the buffer isn't |
| // malformed and code stays semantically equivalent. |
| assert_eq!(new_text, vec!["/* 😢😢😢😢😢😢😢 */\nfn main() {}\n"]); |
| } |
| |
| #[test] |
| fn client_lens_run() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("lens_run")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>( |
| 0, |
| lsp_types::InitializeParams { |
| process_id: None, |
| root_uri: None, |
| root_path: Some(root_path.display().to_string()), |
| initialization_options: Some(json!({ "cmdRun": true})), |
| capabilities: Default::default(), |
| trace: None, |
| workspace_folders: None, |
| }, |
| ); |
| |
| rls.wait_for_indexing(); |
| assert!(rls.messages().iter().count() >= 7); |
| |
| let lens = rls.request::<CodeLensRequest>( |
| 1, |
| CodeLensParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| }, |
| ); |
| |
| let expected = CodeLens { |
| command: Some(Command { |
| command: "rls.run".to_string(), |
| title: "Run test".to_string(), |
| arguments: Some(vec![json!({ |
| "args": [ "test", "--", "--nocapture", "test_foo" ], |
| "binary": "cargo", |
| "env": { "RUST_BACKTRACE": "short" } |
| })]), |
| }), |
| data: None, |
| range: Range { |
| start: Position { line: 4, character: 3 }, |
| end: Position { line: 4, character: 11 }, |
| }, |
| }; |
| |
| assert_eq!(lens, Some(vec![expected])); |
| } |
| |
| #[test] |
| #[ignore] // Spurious in Rust CI, https://github.com/rust-lang/rust/issues/62225 |
| fn client_find_definitions() { |
| const SRC: &str = r#" |
| struct Foo { |
| } |
| |
| impl Foo { |
| fn new() { |
| } |
| } |
| |
| fn main() { |
| Foo::new(); |
| } |
| "#; |
| |
| let p = project("simple_workspace") |
| .file("Cargo.toml", &basic_bin_manifest("bar")) |
| .file("src/main.rs", SRC) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| // FIXME: Without `all_targets=false`, this test will randomly fail. |
| let opts = json!({"settings": {"rust": {"racer_completion": false, "all_targets": false } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let mut results = vec![]; |
| for (line_index, line) in SRC.lines().enumerate() { |
| for i in 0..line.len() { |
| let id = (line_index * 100 + i) as u64; |
| let result = rls.request::<GotoDefinition>( |
| id, |
| TextDocumentPositionParams { |
| position: Position { line: line_index as u64, character: i as u64 }, |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| }, |
| ); |
| |
| let ranges: Vec<_> = result |
| .into_iter() |
| .flat_map(|x| match x { |
| GotoDefinitionResponse::Scalar(loc) => vec![loc].into_iter(), |
| GotoDefinitionResponse::Array(locs) => locs.into_iter(), |
| _ => unreachable!(), |
| }) |
| .map(|x| x.range) |
| .collect(); |
| |
| if !ranges.is_empty() { |
| results.push((line_index, i, ranges)); |
| } |
| } |
| } |
| |
| // Foo |
| let foo_definition = Range { |
| start: Position { line: 1, character: 15 }, |
| end: Position { line: 1, character: 18 }, |
| }; |
| |
| // Foo::new |
| let foo_new_definition = Range { |
| start: Position { line: 5, character: 15 }, |
| end: Position { line: 5, character: 18 }, |
| }; |
| |
| // main |
| let main_definition = Range { |
| start: Position { line: 9, character: 11 }, |
| end: Position { line: 9, character: 15 }, |
| }; |
| |
| let expected = [ |
| // struct Foo |
| (1, 15, vec![foo_definition]), |
| (1, 16, vec![foo_definition]), |
| (1, 17, vec![foo_definition]), |
| (1, 18, vec![foo_definition]), |
| // impl Foo |
| (4, 13, vec![foo_definition]), |
| (4, 14, vec![foo_definition]), |
| (4, 15, vec![foo_definition]), |
| (4, 16, vec![foo_definition]), |
| // fn new |
| (5, 15, vec![foo_new_definition]), |
| (5, 16, vec![foo_new_definition]), |
| (5, 17, vec![foo_new_definition]), |
| (5, 18, vec![foo_new_definition]), |
| // fn main |
| (9, 11, vec![main_definition]), |
| (9, 12, vec![main_definition]), |
| (9, 13, vec![main_definition]), |
| (9, 14, vec![main_definition]), |
| (9, 15, vec![main_definition]), |
| // Foo::new() |
| (10, 12, vec![foo_definition]), |
| (10, 13, vec![foo_definition]), |
| (10, 14, vec![foo_definition]), |
| (10, 15, vec![foo_definition]), |
| (10, 17, vec![foo_new_definition]), |
| (10, 18, vec![foo_new_definition]), |
| (10, 19, vec![foo_new_definition]), |
| (10, 20, vec![foo_new_definition]), |
| ]; |
| |
| if results.len() != expected.len() { |
| panic!( |
| "Got different amount of completions than expected: {} vs. {}: {:#?}", |
| results.len(), |
| expected.len(), |
| results |
| ) |
| } |
| |
| for (i, (actual, expected)) in results.iter().zip(expected.iter()).enumerate() { |
| if actual != expected { |
| panic!( |
| "Found different definition at index {}. Got {:#?}, expected {:#?}", |
| i, actual, expected |
| ) |
| } |
| } |
| } |
| |
| #[test] |
| fn client_deglob() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("deglob")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| // Test a single swglob |
| let commands = rls |
| .request::<CodeActionRequest>( |
| 100, |
| CodeActionParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| range: Range { start: Position::new(2, 0), end: Position::new(2, 0) }, |
| context: CodeActionContext { diagnostics: vec![], only: None }, |
| }, |
| ) |
| .expect("No code actions returned for line 2"); |
| |
| // Right now we only support deglobbing via commands. Please update this |
| // test if we move to making text edits via CodeAction (which we should for |
| // deglobbing); |
| let Command { title, command, arguments, .. } = match commands { |
| CodeActionResponse::Commands(commands) => commands, |
| CodeActionResponse::Actions(_) => unimplemented!(), |
| } |
| .into_iter() |
| .nth(0) |
| .unwrap(); |
| |
| let arguments = arguments.expect("Missing command arguments"); |
| |
| assert_eq!(title, "Deglob import".to_string()); |
| assert!(command.starts_with("rls.deglobImports-")); |
| |
| assert!(arguments[0]["new_text"].as_str() == Some("{Stdin, Stdout}")); |
| assert_eq!( |
| serde_json::from_value::<Location>(arguments[0]["location"].clone()).unwrap(), |
| Location { |
| range: Range { start: Position::new(2, 13), end: Position::new(2, 14) }, |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| } |
| ); |
| |
| rls.request::<ExecuteCommand>(200, ExecuteCommandParams { command, arguments }); |
| // Right now the execute command returns an empty response and sends |
| // appropriate apply edit request via a side-channel |
| let result = rls |
| .messages() |
| .iter() |
| .rfind(|msg| msg["method"] == ApplyWorkspaceEdit::METHOD) |
| .unwrap() |
| .clone(); |
| let params = <ApplyWorkspaceEdit as Request>::Params::deserialize(&result["params"]) |
| .expect("Couldn't deserialize params"); |
| |
| let (url, edits) = params.edit.changes.unwrap().drain().nth(0).unwrap(); |
| assert_eq!(url, Url::from_file_path(p.root().join("src/main.rs")).unwrap()); |
| assert_eq!( |
| edits, |
| vec![TextEdit { |
| range: Range { start: Position::new(2, 13), end: Position::new(2, 14) }, |
| new_text: "{Stdin, Stdout}".to_string(), |
| }] |
| ); |
| |
| // Test a deglob for double wildcard |
| let commands = rls |
| .request::<CodeActionRequest>( |
| 1100, |
| CodeActionParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| range: Range { start: Position::new(5, 0), end: Position::new(5, 0) }, |
| context: CodeActionContext { diagnostics: vec![], only: None }, |
| }, |
| ) |
| .expect("No code actions returned for line 12"); |
| |
| // Right now we only support deglobbing via commands. Please update this |
| // test if we move to making text edits via CodeAction (which we should for |
| // deglobbing); |
| let Command { title, command, arguments, .. } = match commands { |
| CodeActionResponse::Commands(commands) => commands, |
| CodeActionResponse::Actions(_) => unimplemented!(), |
| } |
| .into_iter() |
| .nth(0) |
| .unwrap(); |
| |
| let arguments = arguments.expect("Missing command arguments"); |
| |
| assert_eq!(title, "Deglob imports".to_string()); |
| assert!(command.starts_with("rls.deglobImports-")); |
| let expected = [(14, 15, "size_of"), (31, 32, "max")]; |
| for i in 0..2 { |
| assert!(arguments[i]["new_text"].as_str() == Some(expected[i].2)); |
| assert_eq!( |
| serde_json::from_value::<Location>(arguments[i]["location"].clone()).unwrap(), |
| Location { |
| range: Range { |
| start: Position::new(5, expected[i].0), |
| end: Position::new(5, expected[i].1), |
| }, |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| } |
| ); |
| } |
| |
| rls.request::<ExecuteCommand>(1200, ExecuteCommandParams { command, arguments }); |
| // Right now the execute command returns an empty response and sends |
| // appropriate apply edit request via a side-channel |
| let result = rls |
| .messages() |
| .iter() |
| .rfind(|msg| msg["method"] == ApplyWorkspaceEdit::METHOD) |
| .unwrap() |
| .clone(); |
| let params = <ApplyWorkspaceEdit as Request>::Params::deserialize(&result["params"]) |
| .expect("Couldn't deserialize params"); |
| |
| let (url, edits) = params.edit.changes.unwrap().drain().nth(0).unwrap(); |
| assert_eq!(url, Url::from_file_path(p.root().join("src/main.rs")).unwrap()); |
| assert_eq!( |
| edits, |
| expected |
| .iter() |
| .map(|e| TextEdit { |
| range: Range { start: Position::new(5, e.0), end: Position::new(5, e.1) }, |
| new_text: e.2.to_string() |
| }) |
| .collect::<Vec<_>>() |
| ); |
| } |
| |
| fn is_notification_for_unknown_config(msg: &serde_json::Value) -> bool { |
| msg["method"] == ShowMessage::METHOD |
| && msg["params"]["message"].as_str().unwrap().contains("Unknown") |
| } |
| |
| fn is_notification_for_duplicated_config(msg: &serde_json::Value) -> bool { |
| msg["method"] == ShowMessage::METHOD |
| && msg["params"]["message"].as_str().unwrap().contains("Duplicate") |
| } |
| |
| #[test] |
| fn client_init_duplicated_and_unknown_settings() { |
| let p = project("simple_workspace") |
| .file("Cargo.toml", &basic_bin_manifest("foo")) |
| .file( |
| "src/main.rs", |
| r#" |
| struct UnusedBin; |
| fn main() { |
| println!("Hello world!"); |
| } |
| "#, |
| ) |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({ |
| "settings": { |
| "rust": { |
| "features": ["some_feature"], |
| "all_targets": false, |
| "unknown1": 1, |
| "unknown2": false, |
| "dup_val": 1, |
| "dup_val": false, |
| "dup_licated": "dup_lacated", |
| "DupLicated": "DupLicated", |
| "dup-licated": "dup-licated" |
| } |
| } |
| }); |
| |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| assert!(rls.messages().iter().any(is_notification_for_unknown_config)); |
| assert!(rls.messages().iter().any(is_notification_for_duplicated_config)); |
| } |
| |
| #[test] |
| fn client_did_change_configuration_duplicated_and_unknown_settings() { |
| let p = project("simple_workspace") |
| .file("Cargo.toml", &basic_bin_manifest("foo")) |
| .file( |
| "src/main.rs", |
| r#" |
| struct UnusedBin; |
| fn main() { |
| println!("Hello world!"); |
| } |
| "#, |
| ) |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| assert!(!rls.messages().iter().any(is_notification_for_unknown_config)); |
| assert!(!rls.messages().iter().any(is_notification_for_duplicated_config)); |
| let settings = json!({ |
| "rust": { |
| "features": ["some_feature"], |
| "all_targets": false, |
| "unknown1": 1, |
| "unknown2": false, |
| "dup_val": 1, |
| "dup_val": false, |
| "dup_licated": "dup_lacated", |
| "DupLicated": "DupLicated", |
| "dup-licated": "dup-licated" |
| } |
| }); |
| rls.notify::<DidChangeConfiguration>(DidChangeConfigurationParams { |
| settings: settings.clone(), |
| }); |
| |
| rls.wait_for_message(is_notification_for_unknown_config); |
| if !rls.messages().iter().any(is_notification_for_duplicated_config) { |
| rls.wait_for_message(is_notification_for_duplicated_config); |
| } |
| } |
| |
| #[test] |
| fn client_shutdown() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| } |
| |
| #[test] |
| fn client_goto_def() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls.request::<GotoDefinition>( |
| 11, |
| TextDocumentPositionParams { |
| position: Position { line: 12, character: 27 }, |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| }, |
| ); |
| |
| let ranges: Vec<_> = result |
| .into_iter() |
| .flat_map(|x| match x { |
| GotoDefinitionResponse::Scalar(loc) => vec![loc].into_iter(), |
| GotoDefinitionResponse::Array(locs) => locs.into_iter(), |
| _ => unreachable!(), |
| }) |
| .map(|x| x.range) |
| .collect(); |
| |
| assert!(ranges.iter().any(|r| r.start == Position { line: 11, character: 8 })); |
| } |
| |
| #[test] |
| fn client_hover() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| // FIXME: Without `all_targets=false`, this test will randomly fail. |
| let opts = json!({"settings": {"rust": { "all_targets": false } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls |
| .request::<HoverRequest>( |
| 11, |
| TextDocumentPositionParams { |
| position: Position { line: 12, character: 27 }, |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| }, |
| ) |
| .unwrap(); |
| |
| let contents = ["&str", "let world = \"world\";"]; |
| let mut contents: Vec<_> = contents.iter().map(ToString::to_string).collect(); |
| let contents = |
| contents.drain(..).map(|value| LanguageString { language: "rust".to_string(), value }); |
| let contents = contents.map(MarkedString::LanguageString).collect(); |
| |
| assert_eq!(result.contents, HoverContents::Array(contents)); |
| } |
| |
| /// Test hover continues to work after the source has moved line |
| #[ignore] // FIXME(#1265): Spurious failure - sometimes we lose the semantic information from Rust - why? |
| #[test] |
| fn client_hover_after_src_line_change() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": {"racer_completion": false } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let world_src_pos = Position { line: 12, character: 27 }; |
| let world_src_pos_after = Position { line: 13, character: 27 }; |
| |
| let result = rls |
| .request::<HoverRequest>( |
| 11, |
| TextDocumentPositionParams { |
| position: world_src_pos, |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| }, |
| ) |
| .unwrap(); |
| |
| let contents = ["&str", "let world = \"world\";"]; |
| let contents: Vec<_> = contents |
| .iter() |
| .map(|value| LanguageString { language: "rust".to_string(), value: value.to_string() }) |
| .map(MarkedString::LanguageString) |
| .collect(); |
| |
| assert_eq!(result.contents, HoverContents::Array(contents.clone())); |
| |
| rls.notify::<DidChangeTextDocument>(DidChangeTextDocumentParams { |
| content_changes: vec![TextDocumentContentChangeEvent { |
| range: Some(Range { |
| start: Position { line: 10, character: 15 }, |
| end: Position { line: 10, character: 15 }, |
| }), |
| range_length: Some(0), |
| text: "\n ".to_string(), |
| }], |
| text_document: VersionedTextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| version: Some(2), |
| }, |
| }); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls |
| .request::<HoverRequest>( |
| 11, |
| TextDocumentPositionParams { |
| position: world_src_pos_after, |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| }, |
| ) |
| .unwrap(); |
| |
| assert_eq!(result.contents, HoverContents::Array(contents)); |
| } |
| |
| #[test] |
| fn client_workspace_symbol() { |
| let p = |
| ProjectBuilder::try_from_fixture(fixtures_dir().join("workspace_symbol")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": { "cfg_test": true } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let symbols = rls |
| .request::<WorkspaceSymbol>(42, WorkspaceSymbolParams { query: "nemo".to_owned() }) |
| .unwrap(); |
| |
| let mut nemos = vec![ |
| ("src/main.rs", "nemo", SymbolKind::Function, 1, 11, 1, 15, Some("x")), |
| ("src/foo.rs", "nemo", SymbolKind::Module, 0, 4, 0, 8, Some("foo")), |
| ]; |
| |
| for (file, name, kind, start_l, start_c, end_l, end_c, container_name) in nemos.drain(..) { |
| let sym = SymbolInformation { |
| name: name.to_string(), |
| kind, |
| container_name: container_name.map(ToString::to_string), |
| location: Location { |
| uri: Url::from_file_path(p.root().join(file)).unwrap(), |
| range: Range { |
| start: Position { line: start_l, character: start_c }, |
| end: Position { line: end_l, character: end_c }, |
| }, |
| }, |
| deprecated: None, |
| }; |
| dbg!(&sym); |
| assert!(symbols.iter().any(|s| *s == sym)); |
| } |
| } |
| |
| #[test] |
| fn client_workspace_symbol_duplicates() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("workspace_symbol_duplicates")) |
| .unwrap() |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": { "cfg_test": true } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let symbols = rls |
| .request::<WorkspaceSymbol>(42, WorkspaceSymbolParams { query: "Frobnicator".to_owned() }) |
| .unwrap(); |
| |
| let symbol = SymbolInformation { |
| name: "Frobnicator".to_string(), |
| kind: SymbolKind::Struct, |
| container_name: Some("a".to_string()), |
| location: Location { |
| uri: Url::from_file_path(p.root().join("src/shared.rs")).unwrap(), |
| range: Range { |
| start: Position { line: 1, character: 7 }, |
| end: Position { line: 1, character: 18 }, |
| }, |
| }, |
| deprecated: None, |
| }; |
| |
| assert_eq!(symbols, vec![symbol]); |
| } |
| |
| #[ignore] // FIXME(#1265): This is spurious (we don't pick up reference under #[cfg(test)])-ed code - why? |
| #[test] |
| fn client_find_all_refs_test() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": {"all_targets": true } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls |
| .request::<References>( |
| 42, |
| ReferenceParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| position: Position { line: 0, character: 7 }, |
| context: ReferenceContext { include_declaration: true }, |
| }, |
| ) |
| .unwrap(); |
| |
| let ranges = [((0, 7), (0, 10)), ((6, 14), (6, 17)), ((14, 15), (14, 18))]; |
| for ((sl, sc), (el, ec)) in &ranges { |
| let range = Range { |
| start: Position { line: *sl, character: *sc }, |
| end: Position { line: *el, character: *ec }, |
| }; |
| |
| dbg!(range); |
| assert!(result.iter().any(|x| x.range == range)); |
| } |
| } |
| |
| #[test] |
| fn client_find_all_refs_no_cfg_test() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("find_all_refs_no_cfg_test")) |
| .unwrap() |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": { "all_targets": false } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls |
| .request::<References>( |
| 42, |
| ReferenceParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| position: Position { line: 0, character: 7 }, |
| context: ReferenceContext { include_declaration: true }, |
| }, |
| ) |
| .unwrap(); |
| |
| let ranges = [((0, 7), (0, 10)), ((13, 15), (13, 18))]; |
| for ((sl, sc), (el, ec)) in &ranges { |
| let range = Range { |
| start: Position { line: *sl, character: *sc }, |
| end: Position { line: *el, character: *ec }, |
| }; |
| |
| dbg!(range); |
| assert!(result.iter().any(|x| x.range == range)); |
| } |
| } |
| |
| #[test] |
| fn client_borrow_error() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("borrow_error")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| let msg = "cannot borrow `x` as mutable more than once at a time"; |
| assert!(diag.diagnostics.iter().any(|diag| diag.message.contains(msg))); |
| } |
| |
| #[test] |
| fn client_highlight() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| // FIXME: Without `all_targets=false`, this test will randomly fail. |
| let opts = json!({"settings": {"rust": { "all_targets": false } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls |
| .request::<DocumentHighlightRequest>( |
| 42, |
| TextDocumentPositionParams { |
| position: Position { line: 12, character: 27 }, |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| }, |
| ) |
| .unwrap(); |
| |
| let ranges = [((11, 8), (11, 13)), ((12, 27), (12, 32))]; |
| for ((sl, sc), (el, ec)) in &ranges { |
| let range = Range { |
| start: Position { line: *sl, character: *sc }, |
| end: Position { line: *el, character: *ec }, |
| }; |
| |
| dbg!(range); |
| assert!(result.iter().any(|x| x.range == range)); |
| } |
| } |
| |
| #[test] |
| fn client_rename() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| // FIXME: Without `all_targets=false`, this test will randomly fail. |
| let opts = json!({"settings": {"rust": { "all_targets": false } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls |
| .request::<Rename>( |
| 42, |
| RenameParams { |
| position: Position { line: 12, character: 27 }, |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| new_name: "foo".to_owned(), |
| }, |
| ) |
| .unwrap(); |
| |
| dbg!(&result); |
| |
| let uri = Url::from_file_path(p.root().join("src/main.rs")).unwrap(); |
| let ranges = [((11, 8), (11, 13)), ((12, 27), (12, 32))]; |
| let ranges = ranges |
| .iter() |
| .map(|((sl, sc), (el, ec))| Range { |
| start: Position { line: *sl, character: *sc }, |
| end: Position { line: *el, character: *ec }, |
| }) |
| .map(|range| TextEdit { range, new_text: "foo".to_string() }); |
| |
| let changes = std::iter::once((uri, ranges.collect())).collect(); |
| |
| assert_eq!(result.changes, Some(changes)); |
| } |
| |
| #[test] |
| fn client_reformat() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("reformat")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls.request::<Formatting>( |
| 42, |
| DocumentFormattingParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| options: FormattingOptions { |
| tab_size: 4, |
| insert_spaces: true, |
| properties: Default::default(), |
| }, |
| }, |
| ); |
| |
| assert_eq!(result.unwrap()[0], TextEdit { |
| range: Range { |
| start: Position { line: 0, character: 0 }, |
| end: Position { line: 2, character: 0 }, |
| }, |
| new_text: "pub mod foo;\npub fn main() {\n let world = \"world\";\n println!(\"Hello, {}!\", world);\n}\n".to_string(), |
| }); |
| } |
| |
| #[test] |
| fn client_reformat_with_range() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("reformat_with_range")) |
| .unwrap() |
| .build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| let result = rls.request::<RangeFormatting>( |
| 42, |
| DocumentRangeFormattingParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| range: Range { |
| start: Position { line: 1, character: 0 }, |
| end: Position { line: 2, character: 0 }, |
| }, |
| options: FormattingOptions { |
| tab_size: 4, |
| insert_spaces: true, |
| properties: Default::default(), |
| }, |
| }, |
| ); |
| |
| let newline = if cfg!(windows) { "\r\n" } else { "\n" }; |
| let formatted = r#"pub fn main() { |
| let world1 = "world"; |
| println!("Hello, {}!", world1); |
| "# |
| .replace("\r", "") |
| .replace("\n", newline); |
| |
| let edits = result.unwrap(); |
| assert_eq!(edits.len(), 2); |
| assert_eq!(edits[0].new_text, formatted); |
| assert_eq!(edits[1].new_text, newline); |
| } |
| |
| #[test] |
| fn client_multiple_binaries() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("multiple_bins")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": { "build_bin": "bin2" } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| { |
| let msgs = rls.messages(); |
| let diags = msgs |
| .iter() |
| .filter(|x| x["method"] == PublishDiagnostics::METHOD) |
| .flat_map(|msg| msg["params"]["diagnostics"].as_array().unwrap()) |
| .map(|diag| diag["message"].as_str().unwrap()) |
| .collect::<Vec<&str>>(); |
| |
| for i in 1..3 { |
| let msg = &format!("unused variable: `bin_name{}`", i); |
| assert!(diags.iter().any(|message| message.starts_with(msg))); |
| } |
| } |
| } |
| |
| #[ignore] // Requires `rust-src` component, which isn't available in Rust CI. |
| #[test] |
| fn client_completion() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| let text_document = |
| TextDocumentIdentifier { uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap() }; |
| |
| let completions = |x: CompletionResponse| match x { |
| CompletionResponse::Array(items) => items, |
| CompletionResponse::List(CompletionList { items, .. }) => items, |
| }; |
| |
| macro_rules! item_eq { |
| ($item:expr, $expected:expr) => {{ |
| let (label, kind, detail) = $expected; |
| ($item.label == *label && $item.kind == *kind && $item.detail == *detail) |
| }}; |
| } |
| |
| let expected = [ |
| // FIXME(https://github.com/rust-lang/rls/issues/1205) - empty " " string |
| ("world", &Some(CompletionItemKind::Variable), &Some("let world = \" \";".to_string())), |
| ("x", &Some(CompletionItemKind::Field), &Some("x: u64".to_string())), |
| ]; |
| |
| let result = rls.request::<Completion>( |
| 11, |
| CompletionParams { |
| text_document: text_document.clone(), |
| position: Position { line: 12, character: 30 }, |
| context: None, |
| }, |
| ); |
| let items = completions(result.unwrap()); |
| assert!(items.iter().any(|item| item_eq!(item, expected[0]))); |
| let result = rls.request::<Completion>( |
| 11, |
| CompletionParams { |
| text_document: text_document.clone(), |
| position: Position { line: 15, character: 30 }, |
| context: None, |
| }, |
| ); |
| let items = completions(result.unwrap()); |
| assert!(items.iter().any(|item| item_eq!(item, expected[1]))); |
| } |
| |
| #[test] |
| fn client_bin_lib_project() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("bin_lib")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": { "cfg_test": true, "build_bin": "bin_lib" } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| let diag: PublishDiagnosticsParams = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("bin_lib/tests/tests.rs")); |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Warning)); |
| assert!(diag.diagnostics[0].message.contains("unused variable: `unused_var`")); |
| } |
| |
| #[test] |
| fn client_infer_lib() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("infer_lib")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("src/lib.rs")); |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Warning)); |
| assert!(diag.diagnostics[0].message.contains("struct is never constructed: `UnusedLib`")); |
| } |
| |
| #[test] |
| fn client_omit_init_build() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| const ID: u64 = 1337; |
| let response = rls.future_msg(|msg| msg["id"] == json!(ID)); |
| |
| let opts = json!({ "omitInitBuild": true }); |
| rls.request::<Initialize>(ID, initialize_params_with_opts(root_path, opts)); |
| |
| // We need to assert that no other messages are received after a short |
| // period of time (e.g. no build progress messages). |
| std::thread::sleep(std::time::Duration::from_secs(1)); |
| rls.block_on(response).unwrap(); |
| |
| assert_eq!(rls.messages().iter().count(), 1); |
| } |
| |
| #[test] |
| fn client_find_impls() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("find_impls")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| |
| let uri = Url::from_file_path(p.root().join("src/main.rs")).unwrap(); |
| |
| let locations = |result: Option<GotoDefinitionResponse>| match result.unwrap() { |
| GotoDefinitionResponse::Scalar(loc) => vec![loc], |
| GotoDefinitionResponse::Array(locations) => locations, |
| GotoDefinitionResponse::Link(mut links) => { |
| links.drain(..).map(|l| Location { uri: l.target_uri, range: l.target_range }).collect() |
| } |
| }; |
| |
| let result = rls.request::<GotoImplementation>( |
| 1, |
| TextDocumentPositionParams { |
| text_document: TextDocumentIdentifier::new(uri.clone()), |
| position: Position { line: 3, character: 7 }, // "Bar" |
| }, |
| ); |
| let expected = [(9, 15, 9, 18), (10, 12, 10, 15)]; |
| let expected = expected.iter().map(|(a, b, c, d)| Location { |
| uri: uri.clone(), |
| range: Range { |
| start: Position { line: *a, character: *b }, |
| end: Position { line: *c, character: *d }, |
| }, |
| }); |
| let locs = locations(result); |
| for exp in expected { |
| assert!(locs.iter().any(|x| *x == exp)); |
| } |
| |
| let result = rls.request::<GotoImplementation>( |
| 1, |
| TextDocumentPositionParams { |
| text_document: TextDocumentIdentifier::new(uri.clone()), |
| position: Position { line: 6, character: 6 }, // "Super" |
| }, |
| ); |
| let expected = [(9, 15, 9, 18), (13, 15, 13, 18)]; |
| let expected = expected.iter().map(|(a, b, c, d)| Location { |
| uri: uri.clone(), |
| range: Range { |
| start: Position { line: *a, character: *b }, |
| end: Position { line: *c, character: *d }, |
| }, |
| }); |
| let locs = locations(result); |
| for exp in expected { |
| assert!(locs.iter().any(|x| *x == exp)); |
| } |
| } |
| |
| #[test] |
| fn client_features() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("features")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": {"features": ["bar", "baz"] } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| let msg = "cannot find struct, variant or union type `Foo` in this scope"; |
| assert!(diag.diagnostics[0].message.contains(msg)); |
| } |
| |
| #[test] |
| fn client_all_features() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("features")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": {"all_features": true } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| rls.wait_for_indexing(); |
| |
| assert_eq!( |
| rls.messages().iter().filter(|x| x["method"] == PublishDiagnostics::METHOD).count(), |
| 0 |
| ); |
| } |
| |
| #[test] |
| fn client_no_default_features() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("features")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": |
| { "no_default_features": true, "features": ["foo", "bar"] } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| let msg = "cannot find struct, variant or union type `Baz` in this scope"; |
| assert!(diag.diagnostics[0].message.contains(msg)); |
| } |
| |
| #[test] |
| fn client_all_targets() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("bin_lib")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({"settings": {"rust": { "cfg_test": true, "all_targets": true } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| let diag: PublishDiagnosticsParams = rls.wait_for_diagnostics(); |
| |
| assert!(diag.uri.as_str().ends_with("bin_lib/tests/tests.rs")); |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Warning)); |
| assert!(diag.diagnostics[0].message.contains("unused variable: `unused_var`")); |
| } |
| |
| /// Handle receiving a notification before the `initialize` request by ignoring and |
| /// continuing to run |
| #[test] |
| fn client_ignore_uninitialized_notification() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| rls.notify::<DidChangeConfiguration>(DidChangeConfigurationParams { settings: json!({}) }); |
| rls.request::<Initialize>(0, initialize_params(root_path)); |
| |
| rls.wait_for_indexing(); |
| } |
| |
| /// Handle receiving requests before the `initialize` request by returning an error response |
| /// and continuing to run |
| #[test] |
| fn client_fail_uninitialized_request() { |
| let p = ProjectBuilder::try_from_fixture(fixtures_dir().join("common")).unwrap().build(); |
| let mut rls = p.spawn_rls_async(); |
| |
| const ID: u64 = 1337; |
| |
| rls.request::<GotoDefinition>( |
| ID, |
| TextDocumentPositionParams { |
| text_document: TextDocumentIdentifier { |
| uri: Url::from_file_path(p.root().join("src/main.rs")).unwrap(), |
| }, |
| position: Position { line: 0, character: 0 }, |
| }, |
| ); |
| |
| let delay = tokio_timer::Delay::new(Instant::now() + Duration::from_secs(1)); |
| rls.block_on(delay).unwrap(); |
| |
| let err = jsonrpc_core::Failure::deserialize(rls.messages().last().unwrap()).unwrap(); |
| assert_eq!(err.id, jsonrpc_core::Id::Num(ID)); |
| assert_eq!(err.error.code, jsonrpc_core::ErrorCode::ServerError(-32002)); |
| assert_eq!(err.error.message, "not yet received `initialize` request"); |
| } |
| |
| // Test that RLS can accept configuration with config keys in 4 different cases: |
| // - mixedCase |
| // - CamelCase |
| // - snake_case |
| // - kebab-case |
| fn client_init_impl(convert_case: fn(&str) -> String) { |
| let p = project("config_cases") |
| .file("Cargo.toml", &basic_bin_manifest("foo")) |
| .file( |
| "src/main.rs", |
| r#" |
| struct NonCfg; |
| #[cfg(test)] |
| struct CfgTest { inner: PathBuf } |
| "#, |
| ) |
| .build(); |
| |
| let root_path = p.root(); |
| let mut rls = p.spawn_rls_async(); |
| |
| let opts = json!({ "settings": { "rust": { convert_case("all_targets"): true } } }); |
| rls.request::<Initialize>(0, initialize_params_with_opts(root_path, opts)); |
| |
| let diag = rls.wait_for_diagnostics(); |
| |
| assert_eq!(diag.diagnostics.len(), 1); |
| assert_eq!(diag.diagnostics[0].severity, Some(DiagnosticSeverity::Error)); |
| let msg = "cannot find type `PathBuf` in this scope"; |
| assert!(diag.diagnostics[0].message.contains(msg)); |
| } |
| |
| #[test] |
| fn client_init_with_configuration_mixed_case() { |
| client_init_impl(heck::MixedCase::to_mixed_case); |
| } |
| |
| #[test] |
| fn client_init_with_configuration_camel_case() { |
| client_init_impl(heck::CamelCase::to_camel_case); |
| } |
| |
| #[test] |
| fn client_init_with_configuration_snake_case() { |
| client_init_impl(heck::SnakeCase::to_snake_case); |
| } |
| |
| #[test] |
| fn client_init_with_configuration_kebab_case() { |
| client_init_impl(heck::KebabCase::to_kebab_case); |
| } |
| |
| #[test] |
| fn client_parse_error_on_malformed_input() { |
| use crate::support::rls_exe; |
| use std::io::{Read, Write}; |
| use std::process::{Command, Stdio}; |
| |
| let mut cmd = Command::new(rls_exe()) |
| .stdin(Stdio::piped()) |
| .stdout(Stdio::piped()) |
| .stderr(Stdio::null()) |
| .spawn() |
| .unwrap(); |
| |
| cmd.stdin.take().unwrap().write_all(b"Malformed input").unwrap(); |
| let mut output = vec![]; |
| cmd.stdout.take().unwrap().read_to_end(&mut output).unwrap(); |
| let output = String::from_utf8(output).unwrap(); |
| |
| assert_eq!(output, "Content-Length: 75\r\n\r\n{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32700,\"message\":\"Parse error\"},\"id\":null}"); |
| |
| // Right now parse errors shutdown the RLS, which we might want to revisit |
| // to provide better fault tolerance. |
| cmd.wait().unwrap(); |
| } |