diff --git a/.gitignore b/.gitignore index b2f26765566..665657d11ba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ .idea target -test_data/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 4d803c76873..32ed961cbee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,14 @@ categories = ["development-tools"] build = "build.rs" +[lib] +name = "rls" +doctest = false + +[[bin]] +name = "rls" +test = false + [dependencies] cargo = { git = "https://github.com/rust-lang/cargo", rev = "2a9f16da7ffc4877aacf7547bac705f0d82de2d6" } cargo_metadata = "0.6" diff --git a/build.rs b/build.rs index 72ee7376fe8..095258c7ed5 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,9 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +use std::path::Path; +use std::env; + fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-env-changed=CFG_RELEASE_CHANNEL"); @@ -20,4 +23,8 @@ fn main() { "cargo:rustc-env=COMMIT_DATE={}", rustc_tools_util::get_commit_date().unwrap_or_default() ); + println!( + "cargo:rustc-env=FIXTURES_DIR={}", + Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("tests/fixtures").display() + ); } diff --git a/src/actions/diagnostics.rs b/src/actions/diagnostics.rs index 8e5c82903a4..e16246f2658 100644 --- a/src/actions/diagnostics.rs +++ b/src/actions/diagnostics.rs @@ -344,15 +344,18 @@ impl IsWithin for Range { /// Tests for formatted messages from the compilers json output /// run cargo with `--message-format=json` to generate the json for new tests and add .json -/// message files to '$(crate::test::FIXTURES_DIR)/compiler_message/' +/// message files to '$FIXTURES_DIR/compiler_message/' #[cfg(test)] mod diagnostic_message_test { use super::*; use languageserver_types::Position; - pub(super) use crate::test::FIXTURES_DIR; + + pub(super) fn fixtures_dir() -> &'static Path { + Path::new(env!("FIXTURES_DIR")) + } pub(super) fn read_fixture(path: impl AsRef) -> String { - std::fs::read_to_string(FIXTURES_DIR.join(path.as_ref())).unwrap() + std::fs::read_to_string(fixtures_dir().join(path.as_ref())).unwrap() } pub(super) fn parse_compiler_message( diff --git a/src/actions/hover.rs b/src/actions/hover.rs index ecffa0dbcbb..f1b3b1b2dcc 100644 --- a/src/actions/hover.rs +++ b/src/actions/hover.rs @@ -541,7 +541,7 @@ fn create_tooltip( /// /// # Examples /// -/// ``` +/// ```ignore /// # use std::path::Path; /// /// let base_path = Path::new(".rustup/toolchains/nightly-x86_64-pc-windows-msvc/lib/rustlib/src/rust/src/liballoc/string.rs"); @@ -580,7 +580,7 @@ fn skip_path_components>( /// /// # Example /// -/// ``` +/// ```ignore /// # use std::path::PathBuf; /// /// let path = PathBuf::from("libstd/../liballoc/string.rs"); @@ -972,335 +972,10 @@ pub fn tooltip( #[allow(clippy::expect_fun_call)] pub mod test { use super::*; - use crate::actions::format::Rustfmt; - use crate::build::BuildPriority; - use crate::config; - use crate::lsp_data::{ClientCapabilities, InitializationOptions}; - use crate::lsp_data::{Position, TextDocumentIdentifier, TextDocumentPositionParams}; - use crate::server::{Output, RequestId}; - use crate::test::FIXTURES_DIR; - use rls_analysis as analysis; - use serde_derive::{Deserialize, Serialize}; - use serde_json as json; - use url::Url; - - use std::env; - use std::fs; - use std::path::PathBuf; - use std::process; - use std::sync::{Arc, Mutex}; - use std::fmt; - - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] - pub struct Test { - /// Relative to the project _source_ dir (e.g. relative to FIXTURES_DIR/hover/src) - pub file: String, - /// One-based line number - pub line: u64, - /// One-based column number - pub col: u64, - } - - impl Test { - fn load_result(&self, dir: &Path) -> Result { - let path = self.path(dir); - let file = fs::File::open(path.clone()) - .map_err(|e| format!("failed to open hover test result: {:?} ({:?})", path, e))?; - let result: Result = json::from_reader(file).map_err(|e| { - format!( - "failed to deserialize hover test result: {:?} ({:?})", - path, e - ) - }); - result - } - } - - #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] - struct TestResult { - test: Test, - data: Result, String>, - } - - impl TestResult { - fn save(&self, result_dir: &Path) -> Result<(), String> { - let path = self.test.path(result_dir); - let data = json::to_string_pretty(&self).map_err(|e| { - format!( - "failed to serialize hover test result: {:?} ({:?})", - path, e - ) - })?; - fs::write(path.clone(), data) - .map_err(|e| format!("failed to save hover test result: {:?} ({:?})", path, e)) - } - - /// Returns true if data is equal to `other` relaxed so that - /// `MarkedString::String` in `other` need only start with self's. - fn has_same_data_start(&self, other: &Self) -> bool { - match (&self.data, &other.data) { - (Ok(data), Ok(them)) if data.len() == them.len() => data - .iter() - .zip(them.iter()) - .map(|(us, them)| match (us, them) { - (MarkedString::String(us), MarkedString::String(them)) => { - them.starts_with(us) - } - _ => us == them, - }) - .all(|r| r), - _ => false, - } - } - } - - impl Test { - pub fn new(file: &str, line: u64, col: u64) -> Test { - Test { - file: file.into(), - line, - col, - } - } - - fn path(&self, result_dir: &Path) -> PathBuf { - result_dir.join(format!( - "{}.{:04}_{:03}.json", - self.file, self.line, self.col - )) - } - - fn run(&self, project_dir: &Path, ctx: &InitActionContext) -> TestResult { - let url = - Url::from_file_path(project_dir.join("src").join(&self.file)).expect(&self.file); - let doc_id = TextDocumentIdentifier::new(url); - let position = Position::new(self.line - 1u64, self.col - 1u64); - let params = TextDocumentPositionParams::new(doc_id, position); - let result = tooltip(&ctx, ¶ms).map_err(|e| format!("tooltip error: {:?}", e)); - - TestResult { - test: self.clone(), - data: result, - } - } - } - - #[derive(PartialEq, Eq)] - pub struct TestFailure { - /// The test case, indicating file, line, and column - pub test: Test, - /// The location of the loaded result input. - pub expect_file: PathBuf, - /// The location of the saved result output. - pub actual_file: PathBuf, - /// The expected outcome. The outer `Result` relates to errors while - /// loading saved data. The inner `Result` is the saved output from - /// `hover::tooltip`. - pub expect_data: Result, String>, String>, - /// The current output from `hover::tooltip`. The inner `Result` - /// is the output from `hover::tooltip`. - pub actual_data: Result, String>, ()>, - } - - impl fmt::Debug for TestFailure { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt.debug_struct("TestFailure") - .field("test", &self.test) - .field("expect_file", &self.expect_file) - .field("actual_file", &self.actual_file) - .field("expect_data", &self.expect_data) - .field("actual_data", &self.actual_data) - .finish()?; - - let expected = format!("{:#?}", self.expect_data); - let actual = format!("{:#?}", self.actual_data); - write!(fmt, "-diff: {}", difference::Changeset::new(&expected, &actual, "")) - } - } - - - #[derive(Clone, Default)] - pub struct LineOutput { - req_id: Arc>, - lines: Arc>>, - } - - impl LineOutput { - /// Clears and returns the recorded output lines - pub fn reset(&self) -> Vec { - let mut lines = self.lines.lock().unwrap(); - let mut swapped = Vec::new(); - ::std::mem::swap(&mut *lines, &mut swapped); - swapped - } - } - - impl Output for LineOutput { - fn response(&self, output: String) { - self.lines.lock().unwrap().push(output); - } - - fn provide_id(&self) -> RequestId { - let mut id = self.req_id.lock().unwrap(); - *id += 1; - RequestId::Num(*id as u64) - } - } - - pub struct TooltipTestHarness { - ctx: InitActionContext, - project_dir: PathBuf, - working_dir: PathBuf, - } - - impl TooltipTestHarness { - /// Creates a new `TooltipTestHarness`. The `project_dir` must contain - /// a valid rust project with a `Cargo.toml`. - pub fn new( - project_dir: PathBuf, - output: &O, - racer_fallback_completion: bool, - ) -> TooltipTestHarness { - use env_logger; - let _ = env_logger::try_init(); - - // Prevent the hover test project build from trying to use the rls test - // binary as a rustc shim. See RlsExecutor::exec for more information. - if env::var("RUSTC").is_err() { - env::set_var("RUSTC", "rustc"); - } - - let pid = process::id(); - let client_caps = ClientCapabilities { - code_completion_has_snippet_support: true, - related_information_support: true, - }; - let mut config = config::Config::default(); - - let temp_dir = tempfile::tempdir().unwrap().into_path(); - config.target_dir = config::Inferrable::Specified(Some(temp_dir.clone())); - config.racer_completion = racer_fallback_completion; - // FIXME(#1195): This led to spurious failures on macOS; possibly - // because regular build and #[cfg(test)] did race or rls-analysis - // didn't lower them properly? - config.all_targets = false; - - let config = Arc::new(Mutex::new(config)); - let analysis = Arc::new(analysis::AnalysisHost::new(analysis::Target::Debug)); - let vfs = Arc::new(Vfs::new()); - - let ctx = InitActionContext::new( - analysis, - vfs, - config, - client_caps, - project_dir.clone(), - pid, - true, - ); - - ctx.init(InitializationOptions::default(), output); - ctx.build(&project_dir, BuildPriority::Immediate, output); - - TooltipTestHarness { - ctx, - project_dir, - working_dir: temp_dir - } - } - - /// Execute a series of tooltip tests. The test results will be saved in `save_dir`. - /// Each test will attempt to load a previous result from the `load_dir` and compare - /// the results. If a matching file can't be found or the compared data mismatches, - /// the test case fails. The output file names are derived from the source filename, - /// line number, and column. The execution will return an `Err` if either the save or - /// load directories do not exist nor could be created. - pub fn run_tests( - &self, - tests: &[Test], - load_dir: PathBuf, - save_dir: PathBuf, - ) -> Result, String> { - fs::create_dir_all(&load_dir).map_err(|e| { - format!( - "load_dir does not exist and could not be created: {:?} ({:?})", - load_dir, e - ) - })?; - fs::create_dir_all(&save_dir).map_err(|e| { - format!( - "save_dir does not exist and could not be created: {:?} ({:?})", - save_dir, e - ) - })?; - self.ctx.block_on_build(); - - let results: Vec = tests - .iter() - .map(|test| { - let result = test.run(&self.project_dir, &self.ctx); - result.save(&save_dir).unwrap(); - result - }).collect(); - - let failures: Vec = results - .into_iter() - .map(|actual_result| { - match actual_result.test.load_result(&load_dir) { - Ok(expect_result) => { - if actual_result.test != expect_result.test { - let e = format!("Mismatched test: {:?}", expect_result.test); - Some((Err(e), actual_result)) - } else if expect_result.has_same_data_start(&actual_result) { - None - } else { - Some((Ok(expect_result), actual_result)) - } - } - Err(e) => Some((Err(e), actual_result)), - } - }).filter(|failed_result| failed_result.is_some()) - .map(|failed_result| failed_result.unwrap()) - .map(|failed_result| match failed_result { - (Ok(expect_result), actual_result) => { - let load_file = actual_result.test.path(&load_dir); - let save_file = actual_result.test.path(&save_dir); - TestFailure { - test: actual_result.test, - expect_data: Ok(expect_result.data), - expect_file: load_file, - actual_data: Ok(actual_result.data), - actual_file: save_file, - } - } - (Err(e), actual_result) => { - let load_file = actual_result.test.path(&load_dir); - let save_file = actual_result.test.path(&save_dir); - TestFailure { - test: actual_result.test, - expect_data: Err(e), - expect_file: load_file, - actual_data: Ok(actual_result.data), - actual_file: save_file, - } - } - }).collect(); - Ok(failures) - } - } - - impl Drop for TooltipTestHarness { - fn drop(&mut self) { - if let Ok(mut jobs) = self.ctx.jobs.lock() { - jobs.wait_for_all(); - } - - if fs::metadata(&self.working_dir).is_ok() { - fs::remove_dir_all(&self.working_dir).expect("failed to tidy up"); - } - } + pub fn fixtures_dir() -> &'static Path { + Path::new(env!("FIXTURES_DIR")) } /// Strips indentation from string literals by examining @@ -1658,7 +1333,7 @@ pub mod test { #[test] fn test_extract_decl() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_decl.rs"); + let file = fixtures_dir().join("hover/src/test_extract_decl.rs"); let expected = "pub fn foo() -> Foo"; let row_start = Row::new_zero_indexed(10); @@ -1823,7 +1498,7 @@ pub mod test { #[test] fn test_extract_decl_multiline_empty_function() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_decl_multiline_empty_function.rs"); + let file = fixtures_dir().join("hover/src/test_extract_decl_multiline_empty_function.rs"); let expected = noindent( " @@ -1845,7 +1520,7 @@ pub mod test { #[test] fn test_extract_docs_module_docs_with_attribute() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_docs_module_docs_with_attribute.rs"); + let file = fixtures_dir().join("hover/src/test_extract_docs_module_docs_with_attribute.rs"); let row_start = Row::new_zero_indexed(0); let actual = extract_docs(&vfs, &file, row_start) .expect(&format!("failed to extract docs: {:?}", file)) @@ -1869,7 +1544,7 @@ pub mod test { #[test] fn test_extract_docs_module_docs_no_copyright() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_docs_module_docs_no_copyright.rs"); + let file = fixtures_dir().join("hover/src/test_extract_docs_module_docs_no_copyright.rs"); let row_start = Row::new_zero_indexed(0); let actual = extract_docs(&vfs, &file, row_start) .expect(&format!("failed to extract docs: {:?}", file)) @@ -1893,7 +1568,7 @@ pub mod test { #[test] fn test_extract_docs_comment_block() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_docs_comment_block.rs"); + let file = fixtures_dir().join("hover/src/test_extract_docs_comment_block.rs"); let row_start = Row::new_zero_indexed(21); let actual = extract_docs(&vfs, &file, row_start) .expect(&format!("failed to extract docs: {:?}", file)) @@ -1917,7 +1592,7 @@ pub mod test { #[test] fn test_extract_docs_empty_line_before_decl() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_docs_empty_line_before_decl.rs"); + let file = fixtures_dir().join("hover/src/test_extract_docs_empty_line_before_decl.rs"); let row_start = Row::new_zero_indexed(18); let actual = extract_docs(&vfs, &file, row_start) .expect(&format!("failed to extract docs: {:?}", file)) @@ -1941,7 +1616,7 @@ pub mod test { #[test] fn test_extract_docs_module_docs() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_docs_module_docs.rs"); + let file = fixtures_dir().join("hover/src/test_extract_docs_module_docs.rs"); let row_start = Row::new_zero_indexed(0); let actual = extract_docs(&vfs, &file, row_start) @@ -1981,7 +1656,7 @@ pub mod test { #[test] fn test_extract_docs_attributes() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_docs_attributes.rs"); + let file = fixtures_dir().join("hover/src/test_extract_docs_attributes.rs"); let row_start = Row::new_zero_indexed(21); let actual = extract_docs(&vfs, &file, row_start) @@ -2025,7 +1700,7 @@ pub mod test { #[test] fn test_extract_docs_comment_first_line() { let vfs = Vfs::new(); - let file = FIXTURES_DIR.join("hover/src/test_extract_docs_comment_first_line.rs"); + let file = fixtures_dir().join("hover/src/test_extract_docs_comment_first_line.rs"); let row_start = Row::new_zero_indexed(1); let actual = extract_docs(&vfs, &file, row_start) @@ -2036,162 +1711,4 @@ pub mod test { assert_eq!(expected, actual); } - - enum RacerFallback { - Yes, - No, - } - - impl From for bool { - fn from(arg: RacerFallback) -> bool { - match arg { - RacerFallback::Yes => true, - RacerFallback::No => false, - } - } - } - - // Common logic used in `test_tooltip_*` tests below - fn run_tooltip_tests( - tests: &[Test], - proj_dir: PathBuf, - racer_completion: RacerFallback, - ) -> Result<(), Box> { - let out = LineOutput::default(); - - let save_dir_guard = tempfile::tempdir().unwrap(); - let save_dir = save_dir_guard.path().to_owned(); - let load_dir = proj_dir.join("save_data"); - - let harness = TooltipTestHarness::new(proj_dir, &out, racer_completion.into()); - - out.reset(); - - let failures = harness.run_tests(tests, load_dir, save_dir)?; - - if failures.is_empty() { - Ok(()) - } else { - eprintln!("{}\n\n", out.reset().join("\n")); - eprintln!( - "Failures (\x1b[91mexpected\x1b[92mactual\x1b[0m): {:#?}\n\n", - failures - ); - Err(format!("{} of {} tooltip tests failed", failures.len(), tests.len()).into()) - } - } - - #[test] - fn test_tooltip() -> Result<(), Box> { - let _ = env_logger::try_init(); - - let tests = vec![ - Test::new("test_tooltip_01.rs", 13, 11), - Test::new("test_tooltip_01.rs", 15, 7), - Test::new("test_tooltip_01.rs", 17, 7), - Test::new("test_tooltip_01.rs", 21, 13), - Test::new("test_tooltip_01.rs", 23, 9), - Test::new("test_tooltip_01.rs", 23, 16), - Test::new("test_tooltip_01.rs", 25, 8), - Test::new("test_tooltip_01.rs", 27, 8), - Test::new("test_tooltip_01.rs", 27, 8), - Test::new("test_tooltip_01.rs", 30, 11), - Test::new("test_tooltip_01.rs", 32, 10), - Test::new("test_tooltip_01.rs", 32, 19), - Test::new("test_tooltip_01.rs", 32, 26), - Test::new("test_tooltip_01.rs", 32, 35), - Test::new("test_tooltip_01.rs", 32, 49), - Test::new("test_tooltip_01.rs", 33, 11), - Test::new("test_tooltip_01.rs", 34, 16), - Test::new("test_tooltip_01.rs", 34, 23), - Test::new("test_tooltip_01.rs", 35, 16), - Test::new("test_tooltip_01.rs", 35, 23), - Test::new("test_tooltip_01.rs", 36, 16), - Test::new("test_tooltip_01.rs", 36, 23), - Test::new("test_tooltip_01.rs", 42, 15), - Test::new("test_tooltip_01.rs", 56, 6), - Test::new("test_tooltip_01.rs", 66, 6), - Test::new("test_tooltip_01.rs", 67, 30), - Test::new("test_tooltip_01.rs", 68, 11), - Test::new("test_tooltip_01.rs", 68, 26), - Test::new("test_tooltip_01.rs", 75, 10), - Test::new("test_tooltip_01.rs", 85, 14), - Test::new("test_tooltip_01.rs", 85, 50), - Test::new("test_tooltip_01.rs", 85, 54), - Test::new("test_tooltip_01.rs", 86, 7), - Test::new("test_tooltip_01.rs", 86, 10), - Test::new("test_tooltip_01.rs", 87, 20), - Test::new("test_tooltip_01.rs", 88, 18), - Test::new("test_tooltip_01.rs", 93, 11), - Test::new("test_tooltip_01.rs", 95, 25), - Test::new("test_tooltip_01.rs", 109, 21), - Test::new("test_tooltip_01.rs", 113, 21), - Test::new("test_tooltip_mod.rs", 22, 14), - Test::new("test_tooltip_mod_use.rs", 11, 14), - Test::new("test_tooltip_mod_use.rs", 12, 14), - Test::new("test_tooltip_mod_use.rs", 12, 25), - Test::new("test_tooltip_mod_use.rs", 13, 28), - ]; - - run_tooltip_tests(&tests, FIXTURES_DIR.join("hover"), RacerFallback::No) - } - - #[test] - fn test_tooltip_racer() -> Result<(), Box> { - let _ = env_logger::try_init(); - - let tests = vec![ - Test::new("test_tooltip_01.rs", 80, 11), - Test::new("test_tooltip_01.rs", 93, 18), - Test::new("test_tooltip_mod_use_external.rs", 11, 7), - Test::new("test_tooltip_mod_use_external.rs", 12, 7), - Test::new("test_tooltip_mod_use_external.rs", 12, 12), - ]; - - run_tooltip_tests(&tests, FIXTURES_DIR.join("hover"), RacerFallback::Yes) - } - - /// Note: This test is ignored as it doesn't work in the rust-lang/rust repo. - /// It is enabled on CI. - /// Run with `cargo test test_tooltip_std -- --ignored` - #[test] - #[ignore] - fn test_tooltip_std() -> Result<(), Box> { - let _ = env_logger::try_init(); - - let tests = vec![ - Test::new("test_tooltip_std.rs", 18, 15), - Test::new("test_tooltip_std.rs", 18, 27), - Test::new("test_tooltip_std.rs", 19, 7), - Test::new("test_tooltip_std.rs", 19, 12), - Test::new("test_tooltip_std.rs", 20, 12), - Test::new("test_tooltip_std.rs", 20, 20), - Test::new("test_tooltip_std.rs", 21, 25), - Test::new("test_tooltip_std.rs", 22, 33), - Test::new("test_tooltip_std.rs", 23, 11), - Test::new("test_tooltip_std.rs", 23, 18), - Test::new("test_tooltip_std.rs", 24, 24), - Test::new("test_tooltip_std.rs", 25, 17), - Test::new("test_tooltip_std.rs", 25, 25), - ]; - - run_tooltip_tests(&tests, FIXTURES_DIR.join("hover"), RacerFallback::No) - } - - /// Note: This test is ignored as it doesn't work in the rust-lang/rust repo. - /// It is enabled on CI. - /// Run with `cargo test test_tooltip_std -- --ignored` - #[test] - #[ignore] - fn test_tooltip_std_racer() -> Result<(), Box> { - let _ = env_logger::try_init(); - - let tests = vec![ - // these test std stuff - Test::new("test_tooltip_mod_use_external.rs", 14, 12), - Test::new("test_tooltip_mod_use_external.rs", 15, 12), - ]; - - run_tooltip_tests(&tests, FIXTURES_DIR.join("hover"), RacerFallback::Yes) - } } diff --git a/src/actions/mod.rs b/src/actions/mod.rs index 2ee9f5aa1c2..2c3150ecb63 100644 --- a/src/actions/mod.rs +++ b/src/actions/mod.rs @@ -381,7 +381,7 @@ impl InitActionContext { } /// Block until any builds and analysis tasks are complete. - fn block_on_build(&self) { + pub fn block_on_build(&self) { self.build_queue.block_on_build(); } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000000..615f178c219 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,47 @@ +// Copyright 2016 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! The Rust Language Server. +//! +//! The RLS provides a server that runs in the background, providing IDEs, +//! editors, and other tools with information about Rust programs. It supports +//! functionality such as 'goto definition', symbol search, reformatting, and +//! code completion, and enables renaming and refactorings. + +#![feature(rustc_private, integer_atomics, drain_filter)] +#![feature(crate_visibility_modifier)] // needed for edition 2018 +#![allow(unknown_lints)] +#![warn(clippy::all, rust_2018_idioms)] +#![allow( + clippy::cyclomatic_complexity, + clippy::too_many_arguments +)] + +pub use rls_analysis::{AnalysisHost, Target}; +pub use rls_vfs::Vfs; + +pub mod actions; +pub mod build; +pub mod cmd; +pub mod concurrency; +pub mod config; +pub mod lsp_data; +pub mod project_model; +pub mod server; + +type Span = rls_span::Span; + +pub const RUSTC_SHIM_ENV_VAR_NAME: &str = "RLS_RUSTC_SHIM"; + +pub fn version() -> String { + use rustc_tools_util::VersionInfo; + + rustc_tools_util::get_version_info!().to_string() +} diff --git a/src/main.rs b/src/main.rs index 8f7456a32f3..a054615618d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,15 +15,6 @@ //! functionality such as 'goto definition', symbol search, reformatting, and //! code completion, and enables renaming and refactorings. -#![feature(rustc_private, integer_atomics, drain_filter)] -#![feature(crate_visibility_modifier)] // needed for edition 2018 -#![allow(unknown_lints)] -#![warn(clippy::all, rust_2018_idioms)] -#![allow( - clippy::cyclomatic_complexity, - clippy::needless_pass_by_value, - clippy::too_many_arguments -)] // See rustc/rustc.rs in rust repo for explanation of stack adjustments. #![feature(link_args)] #[allow(unused_attributes)] @@ -38,38 +29,18 @@ extern "C" {} use log::warn; -use env_logger; -use rustc_tools_util::*; +use rls_rustc as rustc_shim; use std::env; use std::sync::Arc; -use rls_analysis::{AnalysisHost, Target}; -use rls_rustc as rustc_shim; -use rls_vfs::Vfs; - -pub mod actions; -pub mod build; -pub mod cmd; -pub mod concurrency; -pub mod config; -pub mod lsp_data; -pub mod project_model; -pub mod server; - -#[cfg(test)] -mod test; - -const RUSTC_SHIM_ENV_VAR_NAME: &str = "RLS_RUSTC_SHIM"; const RUSTC_WRAPPER_ENV_VAR: &str = "RUSTC_WRAPPER"; -type Span = rls_span::Span; - /// The main entry point to the RLS. Parses CLI arguments and then runs the /// server. pub fn main() { let exit_code = main_inner(); - ::std::process::exit(exit_code); + std::process::exit(exit_code); } fn main_inner() -> i32 { @@ -88,18 +59,15 @@ fn main_inner() -> i32 { env::remove_var(RUSTC_WRAPPER_ENV_VAR); } - if env::var(RUSTC_SHIM_ENV_VAR_NAME) - .map(|v| v != "0") - .unwrap_or(false) - { + if env::var(rls::RUSTC_SHIM_ENV_VAR_NAME).ok().map_or(false, |v| v != "0") { rustc_shim::run(); return 0; } - if let Some(first_arg) = ::std::env::args().nth(1) { + if let Some(first_arg) = env::args().nth(1) { return match first_arg.as_str() { "--version" | "-V" => { - println!("{}", version()); + println!("{}", rls::version()); 0 } "--help" | "-h" => { @@ -107,7 +75,7 @@ fn main_inner() -> i32 { 0 } "--cli" => { - cmd::run(); + rls::cmd::run(); 0 } unknown => { @@ -121,15 +89,10 @@ fn main_inner() -> i32 { }; } - let analysis = Arc::new(AnalysisHost::new(Target::Debug)); - let vfs = Arc::new(Vfs::new()); - - server::run_server(analysis, vfs) -} + let analysis = Arc::new(rls::AnalysisHost::new(rls::Target::Debug)); + let vfs = Arc::new(rls::Vfs::new()); -fn version() -> String { - let version = rustc_tools_util::get_version_info!(); - version.to_string() + rls::server::run_server(analysis, vfs) } fn help() -> &'static str { diff --git a/src/test/lens.rs b/src/test/lens.rs deleted file mode 100644 index 4e36b7d07ff..00000000000 --- a/src/test/lens.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::path::Path; - -use languageserver_types::{CodeLensParams, TextDocumentIdentifier}; -use serde_json; -use url::Url; - -use crate::{ - actions::requests, - server as ls_server, - test::{ - harness::{compare_json, expect_message, expect_series, Environment, ExpectedMessage}, - initialize_with_opts, request, - }, -}; - -#[test] -fn test_lens_run() { - use serde_json::json; - - let mut env = Environment::generate_from_fixture("lens_run"); - - let source_file_path = Path::new("src").join("main.rs"); - - let root_path = env.cache.abs_path(Path::new(".")); - let root_path = root_path.as_os_str().to_str().map(|x| x.to_owned()); - let url = Url::from_file_path(env.cache.abs_path(&source_file_path)) - .expect("couldn't convert file path to URL"); - let text_doc = TextDocumentIdentifier::new(url); - let messages = vec![ - initialize_with_opts( - 0, - root_path, - Some(json!({ "cmdRun": true })), - ).to_string(), - request::( - 100, - CodeLensParams { - text_document: text_doc, - }, - ).to_string(), - ]; - - let (mut server, results, ..) = env.mock_server(messages); - // Initialize and build. - assert_eq!( - ls_server::LsService::handle_message(&mut server), - ls_server::ServerStateChange::Continue - ); - expect_message( - &mut server, - results.clone(), - &ExpectedMessage::new(Some(0)) - .expect_contains(r#""codeLensProvider":{"resolveProvider":false}"#), - ); - - expect_series(&mut server, results.clone(), vec!["progress"]); - - assert_eq!( - ls_server::LsService::handle_message(&mut server), - ls_server::ServerStateChange::Continue - ); - server.wait_for_concurrent_jobs(); - let result: serde_json::Value = - serde_json::from_str(&results.lock().unwrap().remove(0)).unwrap(); - compare_json( - result.get("result").unwrap(), - r#"[{ - "command": { - "command": "rls.run", - "title": "Run test", - "arguments": [{ - "args": [ "test", "--", "--nocapture", "test_foo" ], - "binary": "cargo", - "env": { "RUST_BACKTRACE": "short" } - }] - }, - "range": { - "start": { "character": 3, "line": 14 }, - "end": { "character": 11, "line": 14 } - } - }]"#, - ) -} diff --git a/src/test/harness.rs b/tests/support/harness.rs similarity index 87% rename from src/test/harness.rs rename to tests/support/harness.rs index d8170ee8441..d8f4bcc2e32 100644 --- a/src/test/harness.rs +++ b/tests/support/harness.rs @@ -18,35 +18,31 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; -use crate::config::{Config, Inferrable}; -use crate::server as ls_server; +use rls::config::{Config, Inferrable}; +use rls::server as ls_server; use env_logger; use languageserver_types as ls_types; -use lazy_static::lazy_static; use rls_analysis::{AnalysisHost, Target}; use rls_vfs::Vfs; use serde_json; use walkdir::WalkDir; -lazy_static! { - static ref MANIFEST_DIR: &'static Path = Path::new(env!("CARGO_MANIFEST_DIR")); - pub static ref FIXTURES_DIR: PathBuf = MANIFEST_DIR.join("tests").join("fixtures"); -} +use super::fixtures_dir; -crate struct Environment { - crate config: Option, - crate cache: Cache, - crate target_path: PathBuf, +pub(crate) struct Environment { + pub(crate) config: Option, + pub(crate) cache: Cache, + pub(crate) target_path: PathBuf, } impl Environment { - crate fn generate_from_fixture(fixture_dir: impl AsRef) -> Self { + pub(crate) fn generate_from_fixture(fixture_dir: impl AsRef) -> Self { let _ = env_logger::try_init(); if env::var("RUSTC").is_err() { env::set_var("RUSTC", "rustc"); } - let fixture_dir = FIXTURES_DIR.join(fixture_dir.as_ref()); + let fixture_dir = fixtures_dir().join(fixture_dir.as_ref()); let scratchpad_dir = build_scratchpad_from_fixture(fixture_dir) .expect("Can't copy fixture files to scratchpad"); @@ -67,7 +63,7 @@ impl Environment { } impl Environment { - crate fn with_config(&mut self, f: F) + pub(crate) fn with_config(&mut self, f: F) where F: FnOnce(&mut Config), { @@ -76,7 +72,7 @@ impl Environment { } // Initialize and run the internals of an LS protocol RLS server. - crate fn mock_server( + pub(crate) fn mock_server( &mut self, messages: Vec, ) -> (ls_server::LsService, LsResultList, Arc>) { @@ -162,13 +158,13 @@ impl ls_server::MessageReader for MockMsgReader { type LsResultList = Arc>>; #[derive(Clone)] -crate struct RecordOutput { - crate output: LsResultList, +pub(crate) struct RecordOutput { + pub(crate) output: LsResultList, output_id: Arc>, } impl RecordOutput { - crate fn new() -> RecordOutput { + pub(crate) fn new() -> RecordOutput { RecordOutput { output: Arc::new(Mutex::new(vec![])), // use some distinguishable value @@ -191,20 +187,20 @@ impl ls_server::Output for RecordOutput { } #[derive(Clone, Debug)] -crate struct ExpectedMessage { +pub(crate) struct ExpectedMessage { id: Option, contains: Vec, } impl ExpectedMessage { - crate fn new(id: Option) -> ExpectedMessage { + pub(crate) fn new(id: Option) -> ExpectedMessage { ExpectedMessage { id, contains: vec![], } } - crate fn expect_contains(&mut self, s: &str) -> &mut ExpectedMessage { + pub(crate) fn expect_contains(&mut self, s: &str) -> &mut ExpectedMessage { self.contains.push(s.to_owned()); self } @@ -213,7 +209,7 @@ impl ExpectedMessage { /// This function checks for messages with a series of constraints (expecrations) /// to appear in the buffer, removing valid messages and returning when encountering /// some that didn't meet the expectation -crate fn expect_series( +pub(crate) fn expect_series( server: &mut ls_server::LsService, results: LsResultList, contains: Vec<&str>, @@ -229,7 +225,7 @@ crate fn expect_series( /// /// It panics if the message wasn't valid and removes it from the buffer /// if it was -crate fn expect_message( +pub(crate) fn expect_message( server: &mut ls_server::LsService, results: LsResultList, expected: &ExpectedMessage, @@ -289,7 +285,7 @@ fn try_expect_message( Ok(()) } -crate fn compare_json(actual: &serde_json::Value, expected: &str) { +pub(crate) fn compare_json(actual: &serde_json::Value, expected: &str) { let expected: serde_json::Value = serde_json::from_str(expected).unwrap(); if actual != &expected { panic!( @@ -301,14 +297,14 @@ crate fn compare_json(actual: &serde_json::Value, expected: &str) { } #[derive(Clone, Copy, Debug)] -crate struct Src<'a> { - crate file_name: &'a Path, +pub(crate) struct Src<'a> { + pub(crate) file_name: &'a Path, // 1 indexed - crate line: usize, - crate name: &'a str, + pub(crate) line: usize, + pub(crate) name: &'a str, } -crate fn src<'a>(file_name: &'a Path, line: usize, name: &'a str) -> Src<'a> { +pub(crate) fn src<'a>(file_name: &'a Path, line: usize, name: &'a str) -> Src<'a> { Src { file_name, line, @@ -316,7 +312,7 @@ crate fn src<'a>(file_name: &'a Path, line: usize, name: &'a str) -> Src<'a> { } } -crate struct Cache { +pub(crate) struct Cache { base_path: PathBuf, files: HashMap>, } @@ -329,7 +325,7 @@ impl Cache { } } - crate fn mk_ls_position(&mut self, src: Src<'_>) -> ls_types::Position { + pub(crate) fn mk_ls_position(&mut self, src: Src<'_>) -> ls_types::Position { let line = self.get_line(src); let col = line .find(src.name) @@ -340,14 +336,14 @@ impl Cache { /// Create a range covering the initial position on the line /// /// The line number uses a 0-based index. - crate fn mk_ls_range_from_line(&mut self, line: u64) -> ls_types::Range { + pub(crate) fn mk_ls_range_from_line(&mut self, line: u64) -> ls_types::Range { ls_types::Range::new( ls_types::Position::new(line, 0), ls_types::Position::new(line, 0), ) } - crate fn abs_path(&self, file_name: &Path) -> PathBuf { + pub(crate) fn abs_path(&self, file_name: &Path) -> PathBuf { let result = self .base_path .join(file_name) diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 74907ce1c2e..4d3b3d4aef4 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -14,7 +14,7 @@ use std::env; use std::io::{self, Read, Write}; use std::mem; use std::panic; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdin, Command, Stdio}; use std::str; use std::sync::{Arc, Mutex}; @@ -23,6 +23,23 @@ use std::time::{Duration, Instant}; pub mod project_builder; pub mod paths; +pub mod harness; + +/// Returns a path to directory containing test fixtures. +pub fn fixtures_dir() -> &'static Path { + Path::new(env!("FIXTURES_DIR")) +} + +/// Returns a timeout for waiting for rls stdout messages +/// +/// Env var `RLS_TEST_WAIT_FOR_AGES` allows super long waiting for CI +pub fn rls_timeout() -> Duration { + Duration::from_secs(if std::env::var("RLS_TEST_WAIT_FOR_AGES").is_ok() { + 300 + } else { + 15 + }) +} /// Parse valid LSP stdout into a list of json messages pub fn parse_messages(stdout: &str) -> Vec { diff --git a/tests/tests.rs b/tests/tests.rs index a7d1dbe4e32..50f3303c4ca 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -13,21 +13,15 @@ use serde_json::{self, json}; use std::io::Write; use std::time::Duration; -use self::support::{basic_bin_manifest, RlsStdout}; -use self::support::project_builder::project; +use rls::actions::requests; +use rls::lsp_data::request::Request as _; -mod support; +use self::support::harness::compare_json; +use self::support::project_builder::{project, ProjectBuilder}; +use self::support::{basic_bin_manifest, fixtures_dir, rls_timeout, RlsStdout}; -/// Returns a timeout for waiting for rls stdout messages -/// -/// Env var `RLS_TEST_WAIT_FOR_AGES` allows super long waiting for CI -fn rls_timeout() -> Duration { - Duration::from_secs(if std::env::var("RLS_TEST_WAIT_FOR_AGES").is_ok() { - 300 - } else { - 15 - }) -} +#[allow(dead_code)] +mod support; #[test] fn cmd_test_infer_bin() { @@ -1308,3 +1302,65 @@ fn cmd_format_utf16_range() { rls.shutdown(rls_timeout()); } + +#[test] +fn cmd_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(); + + rls.request( + 0, + "initialize", + Some(json!({ + "rootPath": root_path, + "capabilities": {}, + "initializationOptions": { "cmdRun": true } + })), + ) + .unwrap(); + + let json: Vec<_> = rls + .wait_until_done_indexing(rls_timeout()) + .to_json_messages() + .collect(); + assert!(json.len() >= 7); + + let request_id = 1; + rls.request( + request_id, + requests::CodeLensRequest::METHOD, + Some(json!({ + "textDocument": { + "uri": format!("file://{}/src/main.rs", root_path.display()), + "version": 1 + } + })), + ) + .unwrap(); + + let json = rls.wait_until_json_id(request_id, rls_timeout()); + + compare_json( + &json["result"], + r#"[{ + "command": { + "command": "rls.run", + "title": "Run test", + "arguments": [{ + "args": [ "test", "--", "--nocapture", "test_foo" ], + "binary": "cargo", + "env": { "RUST_BACKTRACE": "short" } + }] + }, + "range": { + "start": { "character": 3, "line": 14 }, + "end": { "character": 11, "line": 14 } + } + }]"#, + ); + + rls.shutdown(rls_timeout()); +} diff --git a/src/test/mod.rs b/tests/tests_old.rs similarity index 99% rename from src/test/mod.rs rename to tests/tests_old.rs index f5e745bfce8..3b268de6bfb 100644 --- a/src/test/mod.rs +++ b/tests/tests_old.rs @@ -7,19 +7,16 @@ // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. -#[macro_use] -mod harness; -mod lens; -use crate::actions::{notifications, requests}; -use crate::config::{Config, Inferrable}; -use crate::server::{self as ls_server, Notification, Request, RequestId, ShutdownRequest}; +use rls::actions::{notifications, requests}; +use rls::config::{Config, Inferrable}; +use rls::server::{self as ls_server, Notification, Request, RequestId, ShutdownRequest}; use jsonrpc_core; use rls_analysis::{AnalysisHost, Target}; use rls_vfs::Vfs; use serde_json::Value; -use self::harness::{ +use self::support::harness::{ compare_json, expect_message, expect_series, src, Environment, ExpectedMessage, RecordOutput, }; @@ -33,7 +30,8 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use url::Url; -pub use self::harness::FIXTURES_DIR; +#[allow(dead_code)] +mod support; fn initialize(id: usize, root_path: Option) -> Request { initialize_with_opts(id, root_path, None) diff --git a/tests/tooltip.rs b/tests/tooltip.rs new file mode 100644 index 00000000000..0b66dc631ab --- /dev/null +++ b/tests/tooltip.rs @@ -0,0 +1,476 @@ +use rls::actions::hover::tooltip; +use rls::actions::{ActionContext, InitActionContext}; +use rls::config; +use rls::lsp_data::{ClientCapabilities, InitializationOptions}; +use rls::lsp_data::MarkedString; +use rls::lsp_data::{Position, TextDocumentIdentifier, TextDocumentPositionParams}; +use rls::server::{Output, RequestId}; +use rls_analysis as analysis; +use rls_vfs::Vfs; +use serde_derive::{Deserialize, Serialize}; +use serde_json as json; +use url::Url; + +use std::env; +use std::fmt; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; + +pub fn fixtures_dir() -> &'static Path { + Path::new(env!("FIXTURES_DIR")) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Test { + /// Relative to the project _source_ dir (e.g. relative to $FIXTURES_DIR/hover/src) + pub file: String, + /// One-based line number + pub line: u64, + /// One-based column number + pub col: u64, +} + +impl Test { + fn load_result(&self, dir: &Path) -> Result { + let path = self.path(dir); + let file = fs::File::open(path.clone()) + .map_err(|e| format!("failed to open hover test result: {:?} ({:?})", path, e))?; + let result: Result = json::from_reader(file).map_err(|e| { + format!( + "failed to deserialize hover test result: {:?} ({:?})", + path, e + ) + }); + result + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +struct TestResult { + test: Test, + data: Result, String>, +} + +impl TestResult { + fn save(&self, result_dir: &Path) -> Result<(), String> { + let path = self.test.path(result_dir); + let data = json::to_string_pretty(&self).map_err(|e| { + format!( + "failed to serialize hover test result: {:?} ({:?})", + path, e + ) + })?; + fs::write(&path, data) + .map_err(|e| format!("failed to save hover test result: {:?} ({:?})", path, e)) + } + + /// Returns true if data is equal to `other` relaxed so that + /// `MarkedString::String` in `other` need only start with self's. + fn has_same_data_start(&self, other: &Self) -> bool { + match (&self.data, &other.data) { + (Ok(data), Ok(them)) if data.len() == them.len() => data + .iter() + .zip(them.iter()) + .map(|(us, them)| match (us, them) { + (MarkedString::String(us), MarkedString::String(them)) => them.starts_with(us), + _ => us == them, + }) + .all(|r| r), + _ => false, + } + } +} + +impl Test { + pub fn new(file: &str, line: u64, col: u64) -> Test { + Test { + file: file.into(), + line, + col, + } + } + + fn path(&self, result_dir: &Path) -> PathBuf { + result_dir.join(format!( + "{}.{:04}_{:03}.json", + self.file, self.line, self.col + )) + } + + fn run(&self, project_dir: &Path, ctx: &InitActionContext) -> TestResult { + let url = Url::from_file_path(project_dir.join("src").join(&self.file)).expect(&self.file); + let doc_id = TextDocumentIdentifier::new(url); + let position = Position::new(self.line - 1u64, self.col - 1u64); + let params = TextDocumentPositionParams::new(doc_id, position); + let result = tooltip(&ctx, ¶ms).map_err(|e| format!("tooltip error: {:?}", e)); + + TestResult { + test: self.clone(), + data: result, + } + } +} + +#[derive(PartialEq, Eq)] +pub struct TestFailure { + /// The test case, indicating file, line, and column + pub test: Test, + /// The location of the loaded result input. + pub expect_file: PathBuf, + /// The location of the saved result output. + pub actual_file: PathBuf, + /// The expected outcome. The outer `Result` relates to errors while + /// loading saved data. The inner `Result` is the saved output from + /// `hover::tooltip`. + pub expect_data: Result, String>, String>, + /// The current output from `hover::tooltip`. The inner `Result` + /// is the output from `hover::tooltip`. + pub actual_data: Result, String>, ()>, +} + +impl fmt::Debug for TestFailure { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("TestFailure") + .field("test", &self.test) + .field("expect_file", &self.expect_file) + .field("actual_file", &self.actual_file) + .field("expect_data", &self.expect_data) + .field("actual_data", &self.actual_data) + .finish()?; + + let expected = format!("{:#?}", self.expect_data); + let actual = format!("{:#?}", self.actual_data); + write!( + fmt, + "-diff: {}", + difference::Changeset::new(&expected, &actual, "") + ) + } +} + +#[derive(Clone, Default)] +pub struct LineOutput { + req_id: Arc>, + lines: Arc>>, +} + +impl LineOutput { + /// Clears and returns the recorded output lines + pub fn reset(&self) -> Vec { + let mut lines = self.lines.lock().unwrap(); + let mut swapped = Vec::new(); + ::std::mem::swap(&mut *lines, &mut swapped); + swapped + } +} + +impl Output for LineOutput { + fn response(&self, output: String) { + self.lines.lock().unwrap().push(output); + } + + fn provide_id(&self) -> RequestId { + let mut id = self.req_id.lock().unwrap(); + *id += 1; + RequestId::Num(*id as u64) + } +} + +pub struct TooltipTestHarness { + ctx: InitActionContext, + project_dir: PathBuf, + _working_dir: tempfile::TempDir, +} + +impl TooltipTestHarness { + /// Creates a new `TooltipTestHarness`. The `project_dir` must contain + /// a valid rust project with a `Cargo.toml`. + pub fn new( + project_dir: PathBuf, + output: &O, + racer_fallback_completion: bool, + ) -> TooltipTestHarness { + let _ = env_logger::try_init(); + + // Prevent the hover test project build from trying to use the rls test + // binary as a rustc shim. See RlsExecutor::exec for more information. + if env::var("RUSTC").is_err() { + env::set_var("RUSTC", "rustc"); + } + + let client_caps = ClientCapabilities { + code_completion_has_snippet_support: true, + related_information_support: true, + }; + + let _working_dir = tempfile::tempdir().expect("Couldn't create tempdir"); + let target_dir = _working_dir.path().to_owned(); + + let config = config::Config { + target_dir: config::Inferrable::Specified(Some(target_dir)), + racer_completion: racer_fallback_completion, + // FIXME(#1195): This led to spurious failures on macOS. + // Possibly because regular build and #[cfg(test)] did race or + // rls-analysis didn't lower them properly? + all_targets: false, + ..Default::default() + }; + + let config = Arc::new(Mutex::new(config)); + let analysis = Arc::new(analysis::AnalysisHost::new(analysis::Target::Debug)); + let vfs = Arc::new(Vfs::new()); + + let ctx = { + let mut ctx = ActionContext::new(analysis, vfs, config); + ctx.init( + project_dir.clone(), + InitializationOptions::default(), + client_caps, + output, + ) + .unwrap(); + ctx.inited().unwrap() + }; + + ctx.block_on_build(); + + TooltipTestHarness { + ctx, + project_dir, + _working_dir, + } + } + + /// Execute a series of tooltip tests. The test results will be saved in `save_dir`. + /// Each test will attempt to load a previous result from the `load_dir` and compare + /// the results. If a matching file can't be found or the compared data mismatches, + /// the test case fails. The output file names are derived from the source filename, + /// line number, and column. The execution will return an `Err` if either the save or + /// load directories do not exist nor could be created. + pub fn run_tests( + &self, + tests: &[Test], + load_dir: PathBuf, + save_dir: PathBuf, + ) -> Result, String> { + fs::create_dir_all(&load_dir).map_err(|e| { + format!( + "load_dir does not exist and could not be created: {:?} ({:?})", + load_dir, e + ) + })?; + fs::create_dir_all(&save_dir).map_err(|e| { + format!( + "save_dir does not exist and could not be created: {:?} ({:?})", + save_dir, e + ) + })?; + + let results: Vec = tests + .iter() + .map(|test| { + let result = test.run(&self.project_dir, &self.ctx); + result.save(&save_dir).unwrap(); + result + }) + .collect(); + + let failures: Vec = results + .into_iter() + .map( + |actual_result: TestResult| match actual_result.test.load_result(&load_dir) { + Ok(expect_result) => { + if actual_result.test != expect_result.test { + let e = format!("Mismatched test: {:?}", expect_result.test); + Some((Err(e), actual_result)) + } else if expect_result.has_same_data_start(&actual_result) { + None + } else { + Some((Ok(expect_result), actual_result)) + } + } + Err(e) => Some((Err(e), actual_result)), + }, + ) + .filter_map(|failed_result| failed_result) + .map(|(result, actual_result)| { + let load_file = actual_result.test.path(&load_dir); + let save_file = actual_result.test.path(&save_dir); + + TestFailure { + test: actual_result.test, + expect_data: result.map(|x| x.data), + expect_file: load_file, + actual_data: Ok(actual_result.data), + actual_file: save_file, + } + }) + .collect(); + + Ok(failures) + } +} + +impl Drop for TooltipTestHarness { + fn drop(&mut self) { + self.ctx.wait_for_concurrent_jobs(); + } +} + +enum RacerFallback { + Yes, + No, +} + +impl From for bool { + fn from(arg: RacerFallback) -> bool { + match arg { + RacerFallback::Yes => true, + RacerFallback::No => false, + } + } +} + +fn run_tooltip_tests( + tests: &[Test], + proj_dir: PathBuf, + racer_completion: RacerFallback, +) -> Result<(), Box> { + let out = LineOutput::default(); + + let save_dir_guard = tempfile::tempdir().unwrap(); + let save_dir = save_dir_guard.path().to_owned(); + let load_dir = proj_dir.join("save_data"); + + let harness = TooltipTestHarness::new(proj_dir, &out, racer_completion.into()); + + out.reset(); + + let failures = harness.run_tests(tests, load_dir, save_dir)?; + + if failures.is_empty() { + Ok(()) + } else { + eprintln!("{}\n\n", out.reset().join("\n")); + eprintln!( + "Failures (\x1b[91mexpected\x1b[92mactual\x1b[0m): {:#?}\n\n", + failures + ); + Err(format!("{} of {} tooltip tests failed", failures.len(), tests.len()).into()) + } +} + +#[test] +fn test_tooltip() -> Result<(), Box> { + let _ = env_logger::try_init(); + + let tests = vec![ + Test::new("test_tooltip_01.rs", 13, 11), + Test::new("test_tooltip_01.rs", 15, 7), + Test::new("test_tooltip_01.rs", 17, 7), + Test::new("test_tooltip_01.rs", 21, 13), + Test::new("test_tooltip_01.rs", 23, 9), + Test::new("test_tooltip_01.rs", 23, 16), + Test::new("test_tooltip_01.rs", 25, 8), + Test::new("test_tooltip_01.rs", 27, 8), + Test::new("test_tooltip_01.rs", 27, 8), + Test::new("test_tooltip_01.rs", 30, 11), + Test::new("test_tooltip_01.rs", 32, 10), + Test::new("test_tooltip_01.rs", 32, 19), + Test::new("test_tooltip_01.rs", 32, 26), + Test::new("test_tooltip_01.rs", 32, 35), + Test::new("test_tooltip_01.rs", 32, 49), + Test::new("test_tooltip_01.rs", 33, 11), + Test::new("test_tooltip_01.rs", 34, 16), + Test::new("test_tooltip_01.rs", 34, 23), + Test::new("test_tooltip_01.rs", 35, 16), + Test::new("test_tooltip_01.rs", 35, 23), + Test::new("test_tooltip_01.rs", 36, 16), + Test::new("test_tooltip_01.rs", 36, 23), + Test::new("test_tooltip_01.rs", 42, 15), + Test::new("test_tooltip_01.rs", 56, 6), + Test::new("test_tooltip_01.rs", 66, 6), + Test::new("test_tooltip_01.rs", 67, 30), + Test::new("test_tooltip_01.rs", 68, 11), + Test::new("test_tooltip_01.rs", 68, 26), + Test::new("test_tooltip_01.rs", 75, 10), + Test::new("test_tooltip_01.rs", 85, 14), + Test::new("test_tooltip_01.rs", 85, 50), + Test::new("test_tooltip_01.rs", 85, 54), + Test::new("test_tooltip_01.rs", 86, 7), + Test::new("test_tooltip_01.rs", 86, 10), + Test::new("test_tooltip_01.rs", 87, 20), + Test::new("test_tooltip_01.rs", 88, 18), + Test::new("test_tooltip_01.rs", 93, 11), + Test::new("test_tooltip_01.rs", 95, 25), + Test::new("test_tooltip_01.rs", 109, 21), + Test::new("test_tooltip_01.rs", 113, 21), + Test::new("test_tooltip_mod.rs", 22, 14), + Test::new("test_tooltip_mod_use.rs", 11, 14), + Test::new("test_tooltip_mod_use.rs", 12, 14), + Test::new("test_tooltip_mod_use.rs", 12, 25), + Test::new("test_tooltip_mod_use.rs", 13, 28), + ]; + + run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::No) +} + +#[test] +fn test_tooltip_racer() -> Result<(), Box> { + let _ = env_logger::try_init(); + + let tests = vec![ + Test::new("test_tooltip_01.rs", 80, 11), + Test::new("test_tooltip_01.rs", 93, 18), + Test::new("test_tooltip_mod_use_external.rs", 11, 7), + Test::new("test_tooltip_mod_use_external.rs", 12, 7), + Test::new("test_tooltip_mod_use_external.rs", 12, 12), + ]; + + run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::Yes) +} + +/// Note: This test is ignored as it doesn't work in the rust-lang/rust repo. +/// It is enabled on CI. +/// Run with `cargo test test_tooltip_std -- --ignored` +#[test] +#[ignore] +fn test_tooltip_std() -> Result<(), Box> { + let _ = env_logger::try_init(); + + let tests = vec![ + Test::new("test_tooltip_std.rs", 18, 15), + Test::new("test_tooltip_std.rs", 18, 27), + Test::new("test_tooltip_std.rs", 19, 7), + Test::new("test_tooltip_std.rs", 19, 12), + Test::new("test_tooltip_std.rs", 20, 12), + Test::new("test_tooltip_std.rs", 20, 20), + Test::new("test_tooltip_std.rs", 21, 25), + Test::new("test_tooltip_std.rs", 22, 33), + Test::new("test_tooltip_std.rs", 23, 11), + Test::new("test_tooltip_std.rs", 23, 18), + Test::new("test_tooltip_std.rs", 24, 24), + Test::new("test_tooltip_std.rs", 25, 17), + Test::new("test_tooltip_std.rs", 25, 25), + ]; + + run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::No) +} + +/// Note: This test is ignored as it doesn't work in the rust-lang/rust repo. +/// It is enabled on CI. +/// Run with `cargo test test_tooltip_std -- --ignored` +#[test] +#[ignore] +fn test_tooltip_std_racer() -> Result<(), Box> { + let _ = env_logger::try_init(); + + let tests = vec![ + // these test std stuff + Test::new("test_tooltip_mod_use_external.rs", 14, 12), + Test::new("test_tooltip_mod_use_external.rs", 15, 12), + ]; + + run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::Yes) +}