1 use rls::actions::hover::tooltip;
2 use rls::actions::{ActionContext, InitActionContext};
3 use rls::config;
4 use rls::lsp_data::MarkedString;
5 use rls::lsp_data::{ClientCapabilities, InitializationOptions};
6 use rls::lsp_data::{Position, TextDocumentIdentifier, TextDocumentPositionParams};
7 use rls::server::{Output, RequestId};
8 use rls_analysis as analysis;
9 use rls_vfs::Vfs;
10 use serde_derive::{Deserialize, Serialize};
11 use serde_json as json;
12 use url::Url;
13 
14 use std::env;
15 use std::fmt;
16 use std::fs;
17 use std::path::{Path, PathBuf};
18 use std::sync::{Arc, Mutex};
19 
fixtures_dir() -> &'static Path20 pub fn fixtures_dir() -> &'static Path {
21     Path::new(env!("FIXTURES_DIR"))
22 }
23 
24 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
25 pub struct Test {
26     /// Relative to the project _source_ dir (e.g. relative to $FIXTURES_DIR/hover/src)
27     pub file: String,
28     /// One-based line number
29     pub line: u64,
30     /// One-based column number
31     pub col: u64,
32 }
33 
34 impl Test {
load_result(&self, dir: &Path) -> Result<TestResult, String>35     fn load_result(&self, dir: &Path) -> Result<TestResult, String> {
36         let path = self.path(dir);
37         let file = fs::File::open(path.clone())
38             .map_err(|e| format!("failed to open hover test result: {:?} ({:?})", path, e))?;
39         let result: Result<TestResult, String> = json::from_reader(file)
40             .map_err(|e| format!("failed to deserialize hover test result: {:?} ({:?})", path, e));
41         result
42     }
43 }
44 
45 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
46 struct TestResult {
47     test: Test,
48     data: Result<Vec<MarkedString>, String>,
49 }
50 
51 impl TestResult {
save(&self, result_dir: &Path) -> Result<(), String>52     fn save(&self, result_dir: &Path) -> Result<(), String> {
53         let path = self.test.path(result_dir);
54         let data = json::to_string_pretty(&self)
55             .map_err(|e| format!("failed to serialize hover test result: {:?} ({:?})", path, e))?;
56         fs::write(&path, data)
57             .map_err(|e| format!("failed to save hover test result: {:?} ({:?})", path, e))
58     }
59 
60     /// Returns true if data is equal to `other` relaxed so that
61     /// `MarkedString::String` in `other` need only start with self's.
has_same_data_start(&self, other: &Self) -> bool62     fn has_same_data_start(&self, other: &Self) -> bool {
63         match (&self.data, &other.data) {
64             (Ok(data), Ok(them)) if data.len() == them.len() => data
65                 .iter()
66                 .zip(them.iter())
67                 .map(|(us, them)| match (us, them) {
68                     (MarkedString::String(us), MarkedString::String(them)) => them.starts_with(us),
69                     _ => us == them,
70                 })
71                 .all(|r| r),
72             _ => false,
73         }
74     }
75 }
76 
77 impl Test {
new(file: &str, line: u64, col: u64) -> Test78     pub fn new(file: &str, line: u64, col: u64) -> Test {
79         Test { file: file.into(), line, col }
80     }
81 
path(&self, result_dir: &Path) -> PathBuf82     fn path(&self, result_dir: &Path) -> PathBuf {
83         result_dir.join(format!("{}.{:04}_{:03}.json", self.file, self.line, self.col))
84     }
85 
run(&self, project_dir: &Path, ctx: &InitActionContext) -> TestResult86     fn run(&self, project_dir: &Path, ctx: &InitActionContext) -> TestResult {
87         let url = Url::from_file_path(project_dir.join("src").join(&self.file)).expect(&self.file);
88         let doc_id = TextDocumentIdentifier::new(url);
89         let position = Position::new(self.line - 1u64, self.col - 1u64);
90         let params = TextDocumentPositionParams::new(doc_id, position);
91         let result = tooltip(&ctx, &params)
92             .map_err(|e| format!("tooltip error: {:?}", e))
93             .map(|v| v.contents);
94 
95         TestResult { test: self.clone(), data: result }
96     }
97 }
98 
99 #[derive(PartialEq, Eq)]
100 pub struct TestFailure {
101     /// The test case, indicating file, line, and column
102     pub test: Test,
103     /// The location of the loaded result input.
104     pub expect_file: PathBuf,
105     /// The location of the saved result output.
106     pub actual_file: PathBuf,
107     /// The expected outcome. The outer `Result` relates to errors while
108     /// loading saved data. The inner `Result` is the saved output from
109     /// `hover::tooltip`.
110     pub expect_data: Result<Result<Vec<MarkedString>, String>, String>,
111     /// The current output from `hover::tooltip`. The inner `Result`
112     /// is the output from `hover::tooltip`.
113     pub actual_data: Result<Result<Vec<MarkedString>, String>, ()>,
114 }
115 
116 impl fmt::Debug for TestFailure {
fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result117     fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
118         fmt.debug_struct("TestFailure")
119             .field("test", &self.test)
120             .field("expect_file", &self.expect_file)
121             .field("actual_file", &self.actual_file)
122             .field("expect_data", &self.expect_data)
123             .field("actual_data", &self.actual_data)
124             .finish()?;
125 
126         let expected = format!("{:#?}", self.expect_data);
127         let actual = format!("{:#?}", self.actual_data);
128         write!(fmt, "-diff: {}", difference::Changeset::new(&expected, &actual, ""))
129     }
130 }
131 
132 #[derive(Clone, Default)]
133 pub struct LineOutput {
134     req_id: Arc<Mutex<u64>>,
135     lines: Arc<Mutex<Vec<String>>>,
136 }
137 
138 impl LineOutput {
139     /// Clears and returns the recorded output lines
reset(&self) -> Vec<String>140     pub fn reset(&self) -> Vec<String> {
141         let mut lines = self.lines.lock().unwrap();
142         let mut swapped = Vec::new();
143         ::std::mem::swap(&mut *lines, &mut swapped);
144         swapped
145     }
146 }
147 
148 impl Output for LineOutput {
response(&self, output: String)149     fn response(&self, output: String) {
150         self.lines.lock().unwrap().push(output);
151     }
152 
provide_id(&self) -> RequestId153     fn provide_id(&self) -> RequestId {
154         let mut id = self.req_id.lock().unwrap();
155         *id += 1;
156         RequestId::Num(*id as u64)
157     }
158 }
159 
160 pub struct TooltipTestHarness {
161     ctx: InitActionContext,
162     project_dir: PathBuf,
163     _working_dir: tempfile::TempDir,
164 }
165 
166 impl TooltipTestHarness {
167     /// Creates a new `TooltipTestHarness`. The `project_dir` must contain
168     /// a valid rust project with a `Cargo.toml`.
new<O: Output>( project_dir: PathBuf, output: &O, racer_fallback_completion: bool, ) -> TooltipTestHarness169     pub fn new<O: Output>(
170         project_dir: PathBuf,
171         output: &O,
172         racer_fallback_completion: bool,
173     ) -> TooltipTestHarness {
174         let _ = env_logger::try_init();
175 
176         // Prevent the hover test project build from trying to use the rls test
177         // binary as a rustc shim. See RlsExecutor::exec for more information.
178         if env::var("RUSTC").is_err() {
179             env::set_var("RUSTC", "rustc");
180         }
181 
182         let client_caps = ClientCapabilities {
183             code_completion_has_snippet_support: true,
184             related_information_support: true,
185         };
186 
187         let _working_dir = tempfile::tempdir().expect("Couldn't create tempdir");
188         let target_dir = _working_dir.path().to_owned();
189 
190         let config = config::Config {
191             target_dir: config::Inferrable::Specified(Some(target_dir)),
192             racer_completion: racer_fallback_completion,
193             // FIXME(#1195): This led to spurious failures on macOS.
194             // Possibly because regular build and #[cfg(test)] did race or
195             // rls-analysis didn't lower them properly?
196             all_targets: false,
197             ..Default::default()
198         };
199 
200         let config = Arc::new(Mutex::new(config));
201         let analysis = Arc::new(analysis::AnalysisHost::new(analysis::Target::Debug));
202         let vfs = Arc::new(Vfs::new());
203 
204         let ctx = {
205             let mut ctx = ActionContext::new(analysis, vfs, config);
206             ctx.init(project_dir.clone(), InitializationOptions::default(), client_caps, output)
207                 .unwrap();
208             ctx.inited().unwrap()
209         };
210 
211         ctx.block_on_build();
212 
213         TooltipTestHarness { ctx, project_dir, _working_dir }
214     }
215 
216     /// Execute a series of tooltip tests. The test results will be saved in `save_dir`.
217     /// Each test will attempt to load a previous result from the `load_dir` and compare
218     /// the results. If a matching file can't be found or the compared data mismatches,
219     /// the test case fails. The output file names are derived from the source filename,
220     /// line number, and column. The execution will return an `Err` if either the save or
221     /// load directories do not exist nor could be created.
run_tests( &self, tests: &[Test], load_dir: PathBuf, save_dir: PathBuf, ) -> Result<Vec<TestFailure>, String>222     pub fn run_tests(
223         &self,
224         tests: &[Test],
225         load_dir: PathBuf,
226         save_dir: PathBuf,
227     ) -> Result<Vec<TestFailure>, String> {
228         fs::create_dir_all(&load_dir).map_err(|e| {
229             format!("load_dir does not exist and could not be created: {:?} ({:?})", load_dir, e)
230         })?;
231         fs::create_dir_all(&save_dir).map_err(|e| {
232             format!("save_dir does not exist and could not be created: {:?} ({:?})", save_dir, e)
233         })?;
234 
235         let results: Vec<TestResult> = tests
236             .iter()
237             .map(|test| {
238                 let result = test.run(&self.project_dir, &self.ctx);
239                 result.save(&save_dir).unwrap();
240                 result
241             })
242             .collect();
243 
244         let failures: Vec<TestFailure> = results
245             .into_iter()
246             .map(|actual_result: TestResult| match actual_result.test.load_result(&load_dir) {
247                 Ok(expect_result) => {
248                     if actual_result.test != expect_result.test {
249                         let e = format!("Mismatched test: {:?}", expect_result.test);
250                         Some((Err(e), actual_result))
251                     } else if expect_result.has_same_data_start(&actual_result) {
252                         None
253                     } else {
254                         Some((Ok(expect_result), actual_result))
255                     }
256                 }
257                 Err(e) => Some((Err(e), actual_result)),
258             })
259             .filter_map(|failed_result| failed_result)
260             .map(|(result, actual_result)| {
261                 let load_file = actual_result.test.path(&load_dir);
262                 let save_file = actual_result.test.path(&save_dir);
263 
264                 TestFailure {
265                     test: actual_result.test,
266                     expect_data: result.map(|x| x.data),
267                     expect_file: load_file,
268                     actual_data: Ok(actual_result.data),
269                     actual_file: save_file,
270                 }
271             })
272             .collect();
273 
274         Ok(failures)
275     }
276 }
277 
278 impl Drop for TooltipTestHarness {
drop(&mut self)279     fn drop(&mut self) {
280         self.ctx.wait_for_concurrent_jobs();
281     }
282 }
283 
284 enum RacerFallback {
285     Yes,
286     No,
287 }
288 
289 impl From<RacerFallback> for bool {
from(arg: RacerFallback) -> bool290     fn from(arg: RacerFallback) -> bool {
291         match arg {
292             RacerFallback::Yes => true,
293             RacerFallback::No => false,
294         }
295     }
296 }
297 
run_tooltip_tests( tests: &[Test], proj_dir: PathBuf, racer_completion: RacerFallback, ) -> Result<(), Box<dyn std::error::Error>>298 fn run_tooltip_tests(
299     tests: &[Test],
300     proj_dir: PathBuf,
301     racer_completion: RacerFallback,
302 ) -> Result<(), Box<dyn std::error::Error>> {
303     let out = LineOutput::default();
304 
305     let save_dir_guard = tempfile::tempdir().unwrap();
306     let save_dir = save_dir_guard.path().to_owned();
307     let load_dir = proj_dir.join("save_data");
308 
309     let harness = TooltipTestHarness::new(proj_dir, &out, racer_completion.into());
310 
311     out.reset();
312 
313     let failures = harness.run_tests(tests, load_dir, save_dir)?;
314 
315     if failures.is_empty() {
316         Ok(())
317     } else {
318         eprintln!("{}\n\n", out.reset().join("\n"));
319         eprintln!("Failures (\x1b[91mexpected\x1b[92mactual\x1b[0m): {:#?}\n\n", failures);
320         Err(format!("{} of {} tooltip tests failed", failures.len(), tests.len()).into())
321     }
322 }
323 
324 #[test]
325 #[ignore] // FIXME: For now these hang in Rust CI, fix me and reenable later
test_tooltip() -> Result<(), Box<dyn std::error::Error>>326 fn test_tooltip() -> Result<(), Box<dyn std::error::Error>> {
327     let _ = env_logger::try_init();
328 
329     let tests = vec![
330         Test::new("test_tooltip_01.rs", 3, 11),
331         Test::new("test_tooltip_01.rs", 5, 7),
332         Test::new("test_tooltip_01.rs", 7, 7),
333         Test::new("test_tooltip_01.rs", 11, 13),
334         Test::new("test_tooltip_01.rs", 13, 9),
335         Test::new("test_tooltip_01.rs", 13, 16),
336         Test::new("test_tooltip_01.rs", 15, 8),
337         Test::new("test_tooltip_01.rs", 17, 8),
338         Test::new("test_tooltip_01.rs", 17, 8),
339         Test::new("test_tooltip_01.rs", 20, 11),
340         Test::new("test_tooltip_01.rs", 22, 10),
341         Test::new("test_tooltip_01.rs", 22, 19),
342         Test::new("test_tooltip_01.rs", 22, 26),
343         Test::new("test_tooltip_01.rs", 22, 35),
344         Test::new("test_tooltip_01.rs", 22, 49),
345         Test::new("test_tooltip_01.rs", 23, 11),
346         Test::new("test_tooltip_01.rs", 24, 16),
347         Test::new("test_tooltip_01.rs", 24, 23),
348         Test::new("test_tooltip_01.rs", 25, 16),
349         Test::new("test_tooltip_01.rs", 25, 23),
350         Test::new("test_tooltip_01.rs", 26, 16),
351         Test::new("test_tooltip_01.rs", 26, 23),
352         Test::new("test_tooltip_01.rs", 32, 15),
353         Test::new("test_tooltip_01.rs", 46, 6),
354         Test::new("test_tooltip_01.rs", 56, 6),
355         Test::new("test_tooltip_01.rs", 57, 30),
356         Test::new("test_tooltip_01.rs", 58, 11),
357         Test::new("test_tooltip_01.rs", 58, 26),
358         Test::new("test_tooltip_01.rs", 65, 10),
359         Test::new("test_tooltip_01.rs", 75, 14),
360         Test::new("test_tooltip_01.rs", 75, 50),
361         Test::new("test_tooltip_01.rs", 75, 54),
362         Test::new("test_tooltip_01.rs", 76, 7),
363         Test::new("test_tooltip_01.rs", 76, 10),
364         Test::new("test_tooltip_01.rs", 77, 20),
365         Test::new("test_tooltip_01.rs", 78, 18),
366         Test::new("test_tooltip_01.rs", 83, 11),
367         Test::new("test_tooltip_01.rs", 85, 25),
368         Test::new("test_tooltip_01.rs", 99, 21),
369         Test::new("test_tooltip_01.rs", 103, 21),
370         Test::new("test_tooltip_mod.rs", 12, 14),
371         Test::new("test_tooltip_mod_use.rs", 1, 14),
372         Test::new("test_tooltip_mod_use.rs", 2, 14),
373         Test::new("test_tooltip_mod_use.rs", 2, 25),
374         Test::new("test_tooltip_mod_use.rs", 3, 28),
375     ];
376 
377     run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::No)
378 }
379 
380 #[test]
381 #[ignore] // FIXME: For now these hang in Rust CI, fix me and reenable later
test_tooltip_racer() -> Result<(), Box<dyn std::error::Error>>382 fn test_tooltip_racer() -> Result<(), Box<dyn std::error::Error>> {
383     let _ = env_logger::try_init();
384 
385     let tests = vec![
386         Test::new("test_tooltip_01.rs", 70, 11),
387         Test::new("test_tooltip_01.rs", 83, 18),
388         Test::new("test_tooltip_mod_use_external.rs", 1, 7),
389         Test::new("test_tooltip_mod_use_external.rs", 2, 7),
390         Test::new("test_tooltip_mod_use_external.rs", 2, 12),
391     ];
392 
393     run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::Yes)
394 }
395 
396 /// Note: This test is ignored as it doesn't work in the rust-lang/rust repo.
397 /// It is enabled on CI.
398 /// Run with `cargo test test_tooltip_std -- --ignored`
399 #[test]
400 #[ignore]
test_tooltip_std() -> Result<(), Box<dyn std::error::Error>>401 fn test_tooltip_std() -> Result<(), Box<dyn std::error::Error>> {
402     let _ = env_logger::try_init();
403 
404     let tests = vec![
405         Test::new("test_tooltip_std.rs", 8, 15),
406         Test::new("test_tooltip_std.rs", 8, 27),
407         Test::new("test_tooltip_std.rs", 9, 7),
408         Test::new("test_tooltip_std.rs", 9, 12),
409         Test::new("test_tooltip_std.rs", 10, 12),
410         Test::new("test_tooltip_std.rs", 10, 20),
411         Test::new("test_tooltip_std.rs", 11, 25),
412         Test::new("test_tooltip_std.rs", 12, 33),
413         Test::new("test_tooltip_std.rs", 13, 11),
414         Test::new("test_tooltip_std.rs", 13, 18),
415         Test::new("test_tooltip_std.rs", 14, 24),
416         Test::new("test_tooltip_std.rs", 15, 17),
417         Test::new("test_tooltip_std.rs", 15, 25),
418     ];
419 
420     run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::No)
421 }
422 
423 /// Note: This test is ignored as it doesn't work in the rust-lang/rust repo.
424 /// It is enabled on CI.
425 /// Run with `cargo test test_tooltip_std -- --ignored`
426 #[test]
427 #[ignore]
test_tooltip_std_racer() -> Result<(), Box<dyn std::error::Error>>428 fn test_tooltip_std_racer() -> Result<(), Box<dyn std::error::Error>> {
429     let _ = env_logger::try_init();
430 
431     let tests = vec![
432         // these test std stuff
433         Test::new("test_tooltip_mod_use_external.rs", 4, 12),
434         Test::new("test_tooltip_mod_use_external.rs", 5, 12),
435     ];
436 
437     run_tooltip_tests(&tests, fixtures_dir().join("hover"), RacerFallback::Yes)
438 }
439