1 use std::{
2 cell::{Cell, RefCell},
3 fs,
4 path::{Path, PathBuf},
5 sync::Once,
6 time::Duration,
7 };
8
9 use crossbeam_channel::{after, select, Receiver};
10 use lsp_server::{Connection, Message, Notification, Request};
11 use lsp_types::{notification::Exit, request::Shutdown, TextDocumentIdentifier, Url};
12 use rust_analyzer::{config::Config, lsp_ext, main_loop};
13 use serde::Serialize;
14 use serde_json::{json, to_string_pretty, Value};
15 use test_utils::FixtureWithProjectMeta;
16 use vfs::AbsPathBuf;
17
18 use crate::testdir::TestDir;
19
20 pub(crate) struct Project<'a> {
21 fixture: &'a str,
22 tmp_dir: Option<TestDir>,
23 roots: Vec<PathBuf>,
24 config: serde_json::Value,
25 }
26
27 impl<'a> Project<'a> {
with_fixture(fixture: &str) -> Project<'_>28 pub(crate) fn with_fixture(fixture: &str) -> Project<'_> {
29 Project {
30 fixture,
31 tmp_dir: None,
32 roots: vec![],
33 config: serde_json::json!({
34 "cargo": {
35 // Loading standard library is costly, let's ignore it by default
36 "sysroot": null,
37 // Can't use test binary as rustc wrapper.
38 "buildScripts": {
39 "useRustcWrapper": false,
40 "enable": false,
41 },
42 },
43 "procMacro": {
44 "enable": false,
45 }
46 }),
47 }
48 }
49
tmp_dir(mut self, tmp_dir: TestDir) -> Project<'a>50 pub(crate) fn tmp_dir(mut self, tmp_dir: TestDir) -> Project<'a> {
51 self.tmp_dir = Some(tmp_dir);
52 self
53 }
54
root(mut self, path: &str) -> Project<'a>55 pub(crate) fn root(mut self, path: &str) -> Project<'a> {
56 self.roots.push(path.into());
57 self
58 }
59
with_config(mut self, config: serde_json::Value) -> Project<'a>60 pub(crate) fn with_config(mut self, config: serde_json::Value) -> Project<'a> {
61 fn merge(dst: &mut serde_json::Value, src: serde_json::Value) {
62 match (dst, src) {
63 (Value::Object(dst), Value::Object(src)) => {
64 for (k, v) in src {
65 merge(dst.entry(k).or_insert(v.clone()), v)
66 }
67 }
68 (dst, src) => *dst = src,
69 }
70 }
71 merge(&mut self.config, config);
72 self
73 }
74
server(self) -> Server75 pub(crate) fn server(self) -> Server {
76 let tmp_dir = self.tmp_dir.unwrap_or_else(TestDir::new);
77 static INIT: Once = Once::new();
78 INIT.call_once(|| {
79 tracing_subscriber::fmt()
80 .with_test_writer()
81 .with_env_filter(tracing_subscriber::EnvFilter::from_env("RA_LOG"))
82 .init();
83 profile::init_from(crate::PROFILE);
84 });
85
86 let FixtureWithProjectMeta { fixture, mini_core, proc_macro_names, toolchain } =
87 FixtureWithProjectMeta::parse(self.fixture);
88 assert!(proc_macro_names.is_empty());
89 assert!(mini_core.is_none());
90 assert!(toolchain.is_none());
91 for entry in fixture {
92 let path = tmp_dir.path().join(&entry.path['/'.len_utf8()..]);
93 fs::create_dir_all(path.parent().unwrap()).unwrap();
94 fs::write(path.as_path(), entry.text.as_bytes()).unwrap();
95 }
96
97 let tmp_dir_path = AbsPathBuf::assert(tmp_dir.path().to_path_buf());
98 let mut roots =
99 self.roots.into_iter().map(|root| tmp_dir_path.join(root)).collect::<Vec<_>>();
100 if roots.is_empty() {
101 roots.push(tmp_dir_path.clone());
102 }
103
104 let mut config = Config::new(
105 tmp_dir_path,
106 lsp_types::ClientCapabilities {
107 workspace: Some(lsp_types::WorkspaceClientCapabilities {
108 did_change_watched_files: Some(
109 lsp_types::DidChangeWatchedFilesClientCapabilities {
110 dynamic_registration: Some(true),
111 relative_pattern_support: None,
112 },
113 ),
114 ..Default::default()
115 }),
116 text_document: Some(lsp_types::TextDocumentClientCapabilities {
117 definition: Some(lsp_types::GotoCapability {
118 link_support: Some(true),
119 ..Default::default()
120 }),
121 code_action: Some(lsp_types::CodeActionClientCapabilities {
122 code_action_literal_support: Some(
123 lsp_types::CodeActionLiteralSupport::default(),
124 ),
125 ..Default::default()
126 }),
127 hover: Some(lsp_types::HoverClientCapabilities {
128 content_format: Some(vec![lsp_types::MarkupKind::Markdown]),
129 ..Default::default()
130 }),
131 ..Default::default()
132 }),
133 window: Some(lsp_types::WindowClientCapabilities {
134 work_done_progress: Some(false),
135 ..Default::default()
136 }),
137 experimental: Some(json!({
138 "serverStatusNotification": true,
139 })),
140 ..Default::default()
141 },
142 roots,
143 );
144 config.update(self.config).expect("invalid config");
145 config.rediscover_workspaces();
146
147 Server::new(tmp_dir, config)
148 }
149 }
150
project(fixture: &str) -> Server151 pub(crate) fn project(fixture: &str) -> Server {
152 Project::with_fixture(fixture).server()
153 }
154
155 pub(crate) struct Server {
156 req_id: Cell<i32>,
157 messages: RefCell<Vec<Message>>,
158 _thread: stdx::thread::JoinHandle,
159 client: Connection,
160 /// XXX: remove the tempdir last
161 dir: TestDir,
162 }
163
164 impl Server {
new(dir: TestDir, config: Config) -> Server165 fn new(dir: TestDir, config: Config) -> Server {
166 let (connection, client) = Connection::memory();
167
168 let _thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
169 .name("test server".to_string())
170 .spawn(move || main_loop(config, connection).unwrap())
171 .expect("failed to spawn a thread");
172
173 Server { req_id: Cell::new(1), dir, messages: Default::default(), client, _thread }
174 }
175
doc_id(&self, rel_path: &str) -> TextDocumentIdentifier176 pub(crate) fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier {
177 let path = self.dir.path().join(rel_path);
178 TextDocumentIdentifier { uri: Url::from_file_path(path).unwrap() }
179 }
180
notification<N>(&self, params: N::Params) where N: lsp_types::notification::Notification, N::Params: Serialize,181 pub(crate) fn notification<N>(&self, params: N::Params)
182 where
183 N: lsp_types::notification::Notification,
184 N::Params: Serialize,
185 {
186 let r = Notification::new(N::METHOD.to_string(), params);
187 self.send_notification(r)
188 }
189
190 #[track_caller]
request<R>(&self, params: R::Params, expected_resp: Value) where R: lsp_types::request::Request, R::Params: Serialize,191 pub(crate) fn request<R>(&self, params: R::Params, expected_resp: Value)
192 where
193 R: lsp_types::request::Request,
194 R::Params: Serialize,
195 {
196 let actual = self.send_request::<R>(params);
197 if let Some((expected_part, actual_part)) = find_mismatch(&expected_resp, &actual) {
198 panic!(
199 "JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
200 to_string_pretty(&expected_resp).unwrap(),
201 to_string_pretty(&actual).unwrap(),
202 to_string_pretty(expected_part).unwrap(),
203 to_string_pretty(actual_part).unwrap(),
204 );
205 }
206 }
207
send_request<R>(&self, params: R::Params) -> Value where R: lsp_types::request::Request, R::Params: Serialize,208 pub(crate) fn send_request<R>(&self, params: R::Params) -> Value
209 where
210 R: lsp_types::request::Request,
211 R::Params: Serialize,
212 {
213 let id = self.req_id.get();
214 self.req_id.set(id.wrapping_add(1));
215
216 let r = Request::new(id.into(), R::METHOD.to_string(), params);
217 self.send_request_(r)
218 }
send_request_(&self, r: Request) -> Value219 fn send_request_(&self, r: Request) -> Value {
220 let id = r.id.clone();
221 self.client.sender.send(r.clone().into()).unwrap();
222 while let Some(msg) = self.recv().unwrap_or_else(|Timeout| panic!("timeout: {r:?}")) {
223 match msg {
224 Message::Request(req) => {
225 if req.method == "client/registerCapability" {
226 let params = req.params.to_string();
227 if ["workspace/didChangeWatchedFiles", "textDocument/didSave"]
228 .into_iter()
229 .any(|it| params.contains(it))
230 {
231 continue;
232 }
233 }
234 panic!("unexpected request: {req:?}")
235 }
236 Message::Notification(_) => (),
237 Message::Response(res) => {
238 assert_eq!(res.id, id);
239 if let Some(err) = res.error {
240 panic!("error response: {err:#?}");
241 }
242 return res.result.unwrap();
243 }
244 }
245 }
246 panic!("no response for {r:?}");
247 }
wait_until_workspace_is_loaded(self) -> Server248 pub(crate) fn wait_until_workspace_is_loaded(self) -> Server {
249 self.wait_for_message_cond(1, &|msg: &Message| match msg {
250 Message::Notification(n) if n.method == "experimental/serverStatus" => {
251 let status = n
252 .clone()
253 .extract::<lsp_ext::ServerStatusParams>("experimental/serverStatus")
254 .unwrap();
255 if status.health != lsp_ext::Health::Ok {
256 panic!("server errored/warned while loading workspace: {:?}", status.message);
257 }
258 status.quiescent
259 }
260 _ => false,
261 })
262 .unwrap_or_else(|Timeout| panic!("timeout while waiting for ws to load"));
263 self
264 }
wait_for_message_cond( &self, n: usize, cond: &dyn Fn(&Message) -> bool, ) -> Result<(), Timeout>265 fn wait_for_message_cond(
266 &self,
267 n: usize,
268 cond: &dyn Fn(&Message) -> bool,
269 ) -> Result<(), Timeout> {
270 let mut total = 0;
271 for msg in self.messages.borrow().iter() {
272 if cond(msg) {
273 total += 1
274 }
275 }
276 while total < n {
277 let msg = self.recv()?.expect("no response");
278 if cond(&msg) {
279 total += 1;
280 }
281 }
282 Ok(())
283 }
recv(&self) -> Result<Option<Message>, Timeout>284 fn recv(&self) -> Result<Option<Message>, Timeout> {
285 let msg = recv_timeout(&self.client.receiver)?;
286 let msg = msg.map(|msg| {
287 self.messages.borrow_mut().push(msg.clone());
288 msg
289 });
290 Ok(msg)
291 }
send_notification(&self, not: Notification)292 fn send_notification(&self, not: Notification) {
293 self.client.sender.send(Message::Notification(not)).unwrap();
294 }
295
path(&self) -> &Path296 pub(crate) fn path(&self) -> &Path {
297 self.dir.path()
298 }
299 }
300
301 impl Drop for Server {
drop(&mut self)302 fn drop(&mut self) {
303 self.request::<Shutdown>((), Value::Null);
304 self.notification::<Exit>(());
305 }
306 }
307
308 struct Timeout;
309
recv_timeout(receiver: &Receiver<Message>) -> Result<Option<Message>, Timeout>310 fn recv_timeout(receiver: &Receiver<Message>) -> Result<Option<Message>, Timeout> {
311 let timeout =
312 if cfg!(target_os = "macos") { Duration::from_secs(300) } else { Duration::from_secs(120) };
313 select! {
314 recv(receiver) -> msg => Ok(msg.ok()),
315 recv(after(timeout)) -> _ => Err(Timeout),
316 }
317 }
318
319 // Comparison functionality borrowed from cargo:
320
321 /// Compares JSON object for approximate equality.
322 /// You can use `[..]` wildcard in strings (useful for OS dependent things such
323 /// as paths). You can use a `"{...}"` string literal as a wildcard for
324 /// arbitrary nested JSON. Arrays are sorted before comparison.
find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)>325 fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a Value, &'a Value)> {
326 match (expected, actual) {
327 (Value::Number(l), Value::Number(r)) if l == r => None,
328 (Value::Bool(l), Value::Bool(r)) if l == r => None,
329 (Value::String(l), Value::String(r)) if lines_match(l, r) => None,
330 (Value::Array(l), Value::Array(r)) => {
331 if l.len() != r.len() {
332 return Some((expected, actual));
333 }
334
335 let mut l = l.iter().collect::<Vec<_>>();
336 let mut r = r.iter().collect::<Vec<_>>();
337
338 l.retain(|l| match r.iter().position(|r| find_mismatch(l, r).is_none()) {
339 Some(i) => {
340 r.remove(i);
341 false
342 }
343 None => true,
344 });
345
346 if !l.is_empty() {
347 assert!(!r.is_empty());
348 Some((l[0], r[0]))
349 } else {
350 assert_eq!(r.len(), 0);
351 None
352 }
353 }
354 (Value::Object(l), Value::Object(r)) => {
355 fn sorted_values(obj: &serde_json::Map<String, Value>) -> Vec<&Value> {
356 let mut entries = obj.iter().collect::<Vec<_>>();
357 entries.sort_by_key(|it| it.0);
358 entries.into_iter().map(|(_k, v)| v).collect::<Vec<_>>()
359 }
360
361 let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k));
362 if !same_keys {
363 return Some((expected, actual));
364 }
365
366 let l = sorted_values(l);
367 let r = sorted_values(r);
368
369 l.into_iter().zip(r).find_map(|(l, r)| find_mismatch(l, r))
370 }
371 (Value::Null, Value::Null) => None,
372 // magic string literal "{...}" acts as wildcard for any sub-JSON
373 (Value::String(l), _) if l == "{...}" => None,
374 _ => Some((expected, actual)),
375 }
376 }
377
378 /// Compare a line with an expected pattern.
379 /// - Use `[..]` as a wildcard to match 0 or more characters on the same line
380 /// (similar to `.*` in a regex).
lines_match(expected: &str, actual: &str) -> bool381 fn lines_match(expected: &str, actual: &str) -> bool {
382 // Let's not deal with / vs \ (windows...)
383 // First replace backslash-escaped backslashes with forward slashes
384 // which can occur in, for example, JSON output
385 let expected = expected.replace(r"\\", "/").replace('\\', "/");
386 let mut actual: &str = &actual.replace(r"\\", "/").replace('\\', "/");
387 for (i, part) in expected.split("[..]").enumerate() {
388 match actual.find(part) {
389 Some(j) => {
390 if i == 0 && j != 0 {
391 return false;
392 }
393 actual = &actual[j + part.len()..];
394 }
395 None => return false,
396 }
397 }
398 actual.is_empty() || expected.ends_with("[..]")
399 }
400
401 #[test]
lines_match_works()402 fn lines_match_works() {
403 assert!(lines_match("a b", "a b"));
404 assert!(lines_match("a[..]b", "a b"));
405 assert!(lines_match("a[..]", "a b"));
406 assert!(lines_match("[..]", "a b"));
407 assert!(lines_match("[..]b", "a b"));
408
409 assert!(!lines_match("[..]b", "c"));
410 assert!(!lines_match("b", "c"));
411 assert!(!lines_match("b", "cb"));
412 }
413