diff options
-rw-r--r-- | .github/workflows/ci.yml | 2 | ||||
-rw-r--r-- | cli/tests/integration_tests_lsp.rs | 148 | ||||
-rw-r--r-- | cli/tests/lsp/code_action_params_deadlock.json | 38 | ||||
-rw-r--r-- | test_util/src/lsp.rs | 155 |
4 files changed, 299 insertions, 44 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ac23cab0..406d1bf2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -210,7 +210,7 @@ jobs: path: | ./target key: | - dummy # Cache never be created for this key. + s0mth1ng_rand0m # Cache never be created for this key. restore-keys: | a-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ hashFiles('Cargo.lock') }}- diff --git a/cli/tests/integration_tests_lsp.rs b/cli/tests/integration_tests_lsp.rs index a6d4f2d29..76041c502 100644 --- a/cli/tests/integration_tests_lsp.rs +++ b/cli/tests/integration_tests_lsp.rs @@ -737,7 +737,7 @@ fn lsp_large_doc_changes() { .unwrap(); client .write_notification( - "textDocument/didChagne", + "textDocument/didChange", json!({ "textDocument": { "uri": "file:///a/file.ts", @@ -1421,6 +1421,152 @@ fn lsp_code_actions_deno_cache() { } #[test] +fn lsp_code_actions_deadlock() { + let mut client = init("initialize_params.json"); + client + .write_notification( + "textDocument/didOpen", + load_fixture("did_open_params_large.json"), + ) + .unwrap(); + let (id, method, _) = client.read_request::<Value>().unwrap(); + assert_eq!(method, "workspace/configuration"); + client + .write_response(id, json!({ "enable": true })) + .unwrap(); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/semanticTokens/full", + json!({ + "textDocument": { + "uri": "file:///a/file.ts" + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + for _ in 0..3 { + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + } + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 444, + "character": 11 + }, + "end": { + "line": 444, + "character": 14 + } + }, + "text": "+++" + } + ] + }), + ) + .unwrap(); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 445, + "character": 4 + }, + "end": { + "line": 445, + "character": 4 + } + }, + "text": "// " + } + ] + }), + ) + .unwrap(); + client + .write_notification( + "textDocument/didChange", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "version": 2 + }, + "contentChanges": [ + { + "range": { + "start": { + "line": 477, + "character": 4 + }, + "end": { + "line": 477, + "character": 9 + } + }, + "text": "error" + } + ] + }), + ) + .unwrap(); + // diagnostics only trigger after changes have elapsed in a separate thread, + // so we need to delay the next messages a little bit to attempt to create a + // potential for a deadlock with the codeAction + std::thread::sleep(std::time::Duration::from_millis(50)); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 609, + "character": 33, + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/codeAction", + load_fixture("code_action_params_deadlock.json"), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + + for _ in 0..3 { + let (method, _) = client.read_notification::<Value>().unwrap(); + assert_eq!(method, "textDocument/publishDiagnostics"); + } + + assert!(client.queue_is_empty()); + shutdown(&mut client); +} + +#[test] fn lsp_completions() { let mut client = init("initialize_params.json"); did_open( diff --git a/cli/tests/lsp/code_action_params_deadlock.json b/cli/tests/lsp/code_action_params_deadlock.json new file mode 100644 index 000000000..be0e317e1 --- /dev/null +++ b/cli/tests/lsp/code_action_params_deadlock.json @@ -0,0 +1,38 @@ +{ + "textDocument": { + "uri": "file:///a/file.ts" + }, + "range": { + "start": { + "line": 441, + "character": 33 + }, + "end": { + "line": 441, + "character": 42 + } + }, + "context": { + "diagnostics": [ + { + "range": { + "start": { + "line": 441, + "character": 33 + }, + "end": { + "line": 441, + "character": 42 + } + }, + "severity": 1, + "code": 7031, + "source": "deno-ts", + "message": "Binding element 'debugFlag' implicitly has an 'any' type." + } + ], + "only": [ + "quickfix" + ] + } +} diff --git a/test_util/src/lsp.rs b/test_util/src/lsp.rs index 52099ebe3..7b9fc5965 100644 --- a/test_util/src/lsp.rs +++ b/test_util/src/lsp.rs @@ -2,6 +2,7 @@ use super::new_deno_dir; +use anyhow::Result; use lazy_static::lazy_static; use regex::Regex; use serde::de; @@ -9,6 +10,7 @@ use serde::Deserialize; use serde::Serialize; use serde_json::json; use serde_json::Value; +use std::collections::VecDeque; use std::io; use std::io::Write; use std::path::Path; @@ -61,7 +63,7 @@ impl<'a> From<&'a [u8]> for LspMessage { } } -fn read_message<R>(reader: &mut R) -> Result<Vec<u8>, anyhow::Error> +fn read_message<R>(reader: &mut R) -> Result<Vec<u8>> where R: io::Read + io::BufRead, { @@ -86,8 +88,12 @@ where } pub struct LspClient { - reader: io::BufReader<ChildStdout>, child: Child, + reader: io::BufReader<ChildStdout>, + /// Used to hold pending messages that have come out of the expected sequence + /// by the harness user which will be sent first when trying to consume a + /// message before attempting to read a new message. + msg_queue: VecDeque<LspMessage>, request_id: u64, start: Instant, writer: io::BufWriter<ChildStdin>, @@ -106,8 +112,51 @@ impl Drop for LspClient { } } +fn notification_result<R>( + method: String, + maybe_params: Option<Value>, +) -> Result<(String, Option<R>)> +where + R: de::DeserializeOwned, +{ + let maybe_params = match maybe_params { + Some(params) => Some(serde_json::from_value(params)?), + None => None, + }; + Ok((method, maybe_params)) +} + +fn request_result<R>( + id: u64, + method: String, + maybe_params: Option<Value>, +) -> Result<(u64, String, Option<R>)> +where + R: de::DeserializeOwned, +{ + let maybe_params = match maybe_params { + Some(params) => Some(serde_json::from_value(params)?), + None => None, + }; + Ok((id, method, maybe_params)) +} + +fn response_result<R>( + maybe_result: Option<Value>, + maybe_error: Option<LspResponseError>, +) -> Result<(Option<R>, Option<LspResponseError>)> +where + R: de::DeserializeOwned, +{ + let maybe_result = match maybe_result { + Some(result) => Some(serde_json::from_value(result)?), + None => None, + }; + Ok((maybe_result, maybe_error)) +} + impl LspClient { - pub fn new(deno_exe: &Path) -> Result<Self, anyhow::Error> { + pub fn new(deno_exe: &Path) -> Result<Self> { let deno_dir = new_deno_dir(); let mut child = Command::new(deno_exe) .env("DENO_DIR", deno_dir.path()) @@ -125,6 +174,7 @@ impl LspClient { Ok(Self { child, + msg_queue: VecDeque::new(), reader, request_id: 1, start: Instant::now(), @@ -136,49 +186,79 @@ impl LspClient { self.start.elapsed() } - fn read(&mut self) -> Result<LspMessage, anyhow::Error> { + pub fn queue_is_empty(&self) -> bool { + self.msg_queue.is_empty() + } + + pub fn queue_len(&self) -> usize { + self.msg_queue.len() + } + + fn read(&mut self) -> Result<LspMessage> { let msg_buf = read_message(&mut self.reader)?; let msg = LspMessage::from(msg_buf.as_slice()); Ok(msg) } - pub fn read_notification<R>( - &mut self, - ) -> Result<(String, Option<R>), anyhow::Error> + pub fn read_notification<R>(&mut self) -> Result<(String, Option<R>)> where R: de::DeserializeOwned, { + if !self.msg_queue.is_empty() { + let mut msg_queue = VecDeque::new(); + loop { + match self.msg_queue.pop_front() { + Some(LspMessage::Notification(method, maybe_params)) => { + return notification_result(method, maybe_params) + } + Some(msg) => msg_queue.push_back(msg), + _ => break, + } + } + self.msg_queue = msg_queue; + } + loop { - if let LspMessage::Notification(method, maybe_params) = self.read()? { - if let Some(p) = maybe_params { - let params = serde_json::from_value(p)?; - return Ok((method, Some(params))); - } else { - return Ok((method, None)); + match self.read() { + Ok(LspMessage::Notification(method, maybe_params)) => { + return notification_result(method, maybe_params) } + Ok(msg) => self.msg_queue.push_back(msg), + Err(err) => return Err(err), } } } - pub fn read_request<R>( - &mut self, - ) -> Result<(u64, String, Option<R>), anyhow::Error> + pub fn read_request<R>(&mut self) -> Result<(u64, String, Option<R>)> where R: de::DeserializeOwned, { + if !self.msg_queue.is_empty() { + let mut msg_queue = VecDeque::new(); + loop { + match self.msg_queue.pop_front() { + Some(LspMessage::Request(id, method, maybe_params)) => { + return request_result(id, method, maybe_params) + } + Some(msg) => msg_queue.push_back(msg), + _ => break, + } + } + self.msg_queue = msg_queue; + } + loop { - if let LspMessage::Request(id, method, maybe_params) = self.read()? { - if let Some(p) = maybe_params { - let params = serde_json::from_value(p)?; - return Ok((id, method, Some(params))); - } else { - return Ok((id, method, None)); + match self.read() { + Ok(LspMessage::Request(id, method, maybe_params)) => { + return request_result(id, method, maybe_params) } + Ok(msg) => self.msg_queue.push_back(msg), + Err(err) => return Err(err), } } } - fn write(&mut self, value: Value) -> Result<(), anyhow::Error> { + fn write(&mut self, value: Value) -> Result<()> { let value_str = value.to_string(); let msg = format!( "Content-Length: {}\r\n\r\n{}", @@ -194,7 +274,7 @@ impl LspClient { &mut self, method: S, params: V, - ) -> Result<(Option<R>, Option<LspResponseError>), anyhow::Error> + ) -> Result<(Option<R>, Option<LspResponseError>)> where S: AsRef<str>, V: Serialize, @@ -209,24 +289,19 @@ impl LspClient { self.write(value)?; loop { - if let LspMessage::Response(id, result, error) = self.read()? { - assert_eq!(id, self.request_id); - self.request_id += 1; - if let Some(r) = result { - let result = serde_json::from_value(r)?; - return Ok((Some(result), error)); - } else { - return Ok((None, error)); + match self.read() { + Ok(LspMessage::Response(id, maybe_result, maybe_error)) => { + assert_eq!(id, self.request_id); + self.request_id += 1; + return response_result(maybe_result, maybe_error); } + Ok(msg) => self.msg_queue.push_back(msg), + Err(err) => return Err(err), } } } - pub fn write_response<V>( - &mut self, - id: u64, - result: V, - ) -> Result<(), anyhow::Error> + pub fn write_response<V>(&mut self, id: u64, result: V) -> Result<()> where V: Serialize, { @@ -238,11 +313,7 @@ impl LspClient { self.write(value) } - pub fn write_notification<S, V>( - &mut self, - method: S, - params: V, - ) -> Result<(), anyhow::Error> + pub fn write_notification<S, V>(&mut self, method: S, params: V) -> Result<()> where S: AsRef<str>, V: Serialize, |