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, ¶ms)
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