Skip to content

Commit

Permalink
LS: Add first tests for the new project model
Browse files Browse the repository at this point in the history
commit-id:7caaac0c
  • Loading branch information
mkaput committed May 9, 2024
1 parent 5609678 commit d4a3a92
Show file tree
Hide file tree
Showing 14 changed files with 282 additions and 21 deletions.
25 changes: 20 additions & 5 deletions crates/cairo-lang-language-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ use cairo_lang_utils::{try_extract_matches, Intern, LookupIntern, OptionHelper,
use serde_json::Value;
use tokio::task::spawn_blocking;
use tower_lsp::jsonrpc::{Error as LSPError, Result as LSPResult};
use tower_lsp::lsp_types::request::Request;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, ClientSocket, LanguageServer, LspService, Server};
use tracing::{debug, error, info, trace_span, warn, Instrument};
use tracing::{debug, error, info, trace, trace_span, warn, Instrument};

use crate::config::Config;
use crate::ide::semantic_highlighting::SemanticTokenKind;
Expand All @@ -102,7 +103,7 @@ mod config;
mod env_config;
mod ide;
mod lang;
mod lsp;
pub mod lsp;
mod project;
mod server;
mod toolchain;
Expand Down Expand Up @@ -291,9 +292,14 @@ struct Backend {

impl Backend {
fn build_service(tricks: Tricks) -> (LspService<Self>, ClientSocket) {
LspService::build(|client| Self::new(client, tricks))
.custom_method("vfs/provide", Self::vfs_provide)
.finish()
let service = LspService::build(|client| Self::new(client, tricks))
.custom_method("vfs/provide", Self::vfs_provide);

#[cfg(feature = "testing")]
let service = service
.custom_method(lsp::methods::test::DebugProjects::METHOD, Self::test_debug_projects);

service.finish()
}

fn new(client: Client, tricks: Tricks) -> Self {
Expand Down Expand Up @@ -595,6 +601,15 @@ impl Backend {
}
}

#[cfg(feature = "testing")]
impl Backend {
async fn test_debug_projects(&self) -> LSPResult<String> {
let projects = self.projects.lock().await;
trace!("debugging projects: {projects:?}");
Ok(format!("{projects:#?}"))
}
}

enum ServerCommands {
Reload,
}
Expand Down
16 changes: 16 additions & 0 deletions crates/cairo-lang-language-server/src/lsp/methods.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! This module contains definitions of custom methods (aka requests or notifications) for the
//! Language Server Protocol that are provided by the Cairo Language Server.

use tower_lsp::lsp_types::request::Request;

#[cfg(feature = "testing")]
pub mod test {
use super::*;

pub struct DebugProjects;
impl Request for DebugProjects {
type Params = ();
type Result = String;
const METHOD: &'static str = "test/debugProjects";
}
}
3 changes: 2 additions & 1 deletion crates/cairo-lang-language-server/src/lsp/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod client_capabilities;
pub(crate) mod client_capabilities;
pub mod methods;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt;
use std::path::{Path, PathBuf};

use anyhow::Context;
Expand Down Expand Up @@ -62,3 +63,9 @@ impl Project for CairoProject {
}
}
}

impl fmt::Debug for CairoProject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CairoProject").field("project_path", &self.project_path).finish()
}
}
7 changes: 7 additions & 0 deletions crates/cairo-lang-language-server/src/project/manager.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::cmp::Reverse;
use std::fmt;
use std::path::Path;

use cairo_lang_compiler::db::RootDatabase;
Expand Down Expand Up @@ -131,6 +132,12 @@ impl ProjectManager {
}
}

impl fmt::Debug for ProjectManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list().entries(&self.projects).finish()
}
}

fn is_cairo_file(path: &Path) -> bool {
path.extension().map_or(false, |ext| ext == CAIRO_FILE_EXTENSION)
}
Expand Down
3 changes: 2 additions & 1 deletion crates/cairo-lang-language-server/src/project/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt::Debug;
use std::path::Path;

use cairo_lang_compiler::db::RootDatabase;
Expand All @@ -14,7 +15,7 @@ mod unmanaged_core_crate;

// TODO(mkaput): Remove `Send` bound when migrating to new threading architecture.
/// A single Cairo project manager.
trait Project: Send {
trait Project: Debug + Send {
/// Gets a list of files that, when changed, should trigger a project reload.
///
/// This list may also include lockfiles.
Expand Down
8 changes: 8 additions & 0 deletions crates/cairo-lang-language-server/src/project/scarb/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::fmt;
use std::path::{Path, PathBuf};

use anyhow::Context;
Expand Down Expand Up @@ -118,3 +119,10 @@ impl Project for ScarbWorkspace {
}
}
}

impl fmt::Debug for ScarbWorkspace {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO(mkaput): Implement this.
todo!();
}
}
1 change: 1 addition & 0 deletions crates/cairo-lang-language-server/tests/e2e/main.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
mod project;
mod semantic_tokens;
mod support;
59 changes: 59 additions & 0 deletions crates/cairo-lang-language-server/tests/e2e/project.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use cairo_lang_language_server::lsp;

use crate::support::normalize::assert_normalized_eq;
use crate::support::sandbox;

#[test]
fn cairo_projects() {
let mut ls = sandbox! {
files {
"project1/cairo_project.toml" => r#"
[crate_roots]
project1 = "src"
[config.global]
edition = "2023_11"
"#,
"project1/src/lib.cairo" => r#"fn main() {}"#,

"project2/cairo_project.toml" => r#"
[crate_roots]
project2 = "src"
[config.global]
edition = "2023_11"
"#,
"project2/src/lib.cairo" => r#"fn main() {}"#,

"project2/subproject/cairo_project.toml" => r#"
[crate_roots]
subproject = "src"
[config.global]
edition = "2023_11"
"#,
"project2/subproject/src/lib.cairo" => r#"fn main() {}"#,
}
};

ls.open_and_wait_for_diagnostics("project1/src/lib.cairo");
ls.open_and_wait_for_diagnostics("project2/src/lib.cairo");
ls.open_and_wait_for_diagnostics("project2/subproject/src/lib.cairo");

let projects = ls.send_request::<lsp::methods::test::DebugProjects>(());
assert_normalized_eq!(
&ls,
projects,
r#"[
CairoProject {
project_path: "[ROOT]/project2/subproject/cairo_project.toml",
},
CairoProject {
project_path: "[ROOT]/project2/cairo_project.toml",
},
CairoProject {
project_path: "[ROOT]/project1/cairo_project.toml",
},
]"#
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ impl Fixture {

/// Introspection methods.
impl Fixture {
pub fn root_path(&self) -> &Path {
self.t.path()
}

pub fn root_url(&self) -> Url {
Url::from_directory_path(self.t.path()).unwrap()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ pub enum Message {
impl Message {
/// Creates a JSON-RPC request message from untyped parts.
pub fn request(method: &'static str, id: Id, params: Value) -> Message {
Message::Request(Request::build(method).id(id).params(params).finish())
let mut b = Request::build(method).id(id);
if !params.is_null() {
b = b.params(params);
}
Message::Request(b.finish())
}

/// Creates a JSON-RPC notification message from untyped parts.
Expand Down
108 changes: 95 additions & 13 deletions crates/cairo-lang-language-server/tests/e2e/support/mock_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use futures::channel::mpsc;
use futures::{join, stream, FutureExt, SinkExt, StreamExt, TryFutureExt};
use lsp_types::{lsp_notification, lsp_request};
use serde_json::Value;
use tokio::time::error::Elapsed;
use tokio::time::timeout;
use tower_lsp::jsonrpc::Request;
use tower_lsp::{lsp_types, ClientSocket, LanguageServer, LspService};
use tower_service::Service;

Expand All @@ -23,7 +23,7 @@ use crate::support::runtime::{AbortOnDrop, GuardedRuntime};
///
/// The language server is terminated abruptly upon dropping of this struct.
/// The `shutdown` request and `exit` notifications are not sent at all.
/// Instead, the Tokio Runtime that is executing the server is being shut down and any running
/// Instead, the Tokio Runtime executing the server is being shut down and any running
/// blocking tasks are given a small period of time to complete.
pub struct MockClient {
fixture: Fixture,
Expand All @@ -35,10 +35,11 @@ pub struct MockClient {
}

impl MockClient {
/// Starts and initializes CairoLS in context of a given fixture and given client capabilities.
/// Starts and initializes CairoLS in the context of a given fixture and given client
/// capabilities.
///
/// Upon completion of this function the language server will be in the _initialized_ state
/// (i.e. the `initialize` request and `initialized` notification both will be completed).
/// Upon completion of this function, the language server will be in the _initialized_ state
/// (i.e., the `initialize` request and `initialized` notification both will be completed).
#[must_use]
pub fn start(fixture: Fixture, capabilities: lsp_types::ClientCapabilities) -> Self {
let rt = GuardedRuntime::start();
Expand Down Expand Up @@ -161,7 +162,7 @@ impl MockClient {
self.input_tx.send(message.clone()).await.expect("failed to send request");

while let Some(response_message) =
self.output.recv().await.unwrap_or_else(|_| panic!("timeout: {message:?}"))
self.output.recv().await.unwrap_or_else(|err| panic!("{err:?}: {message:?}"))
{
match response_message {
Message::Request(res) if res.id().is_none() => {
Expand Down Expand Up @@ -201,6 +202,36 @@ impl MockClient {
self.input_tx.send(message).await.expect("failed to send notification");
})
}

/// Looks for a typed client notification that satisfies the given predicate in message trace
/// or waits for a new one.
pub fn wait_for_notification<N>(&mut self, predicate: impl Fn(&N::Params) -> bool)
where
N: lsp_types::notification::Notification,
{
self.wait_for_rpc_request(|req| {
if req.method() != N::METHOD {
return false;
}
let params = serde_json::from_value(req.params().cloned().unwrap_or_default())
.expect("failed to parse notification params");
predicate(&params)
})
}

/// Looks for a client JSON-RPC request that satisfies the given predicate in message trace
/// or waits for a new one.
pub fn wait_for_rpc_request(&mut self, predicate: impl Fn(&Request) -> bool) {
self.rt.block_on(async {
self.output
.wait_for_message(|message| {
let Message::Request(req) = message else { return false };
predicate(req)
})
.await
.unwrap_or_else(|err| panic!("waiting for request failed: {err:?}"))
})
}
}

/// Quality of life helpers for interacting with the server.
Expand Down Expand Up @@ -233,10 +264,44 @@ impl MockClient {
},
)
}

/// Waits for `textDocument/publishDiagnostics` notification for the given file.
pub fn wait_for_diagnostics(&mut self, path: impl AsRef<Path>) {
let uri = self.fixture.file_url(&path);
self.wait_for_notification::<lsp_notification!("textDocument/publishDiagnostics")>(
|params| params.uri == uri,
)
}

/// Sends `textDocument/didOpen` notification to the server and then waits for matching
/// `textDocument/publishDiagnostics` notification.
pub fn open_and_wait_for_diagnostics(&mut self, path: impl AsRef<Path>) {
let path = path.as_ref();
self.open(&path);
self.wait_for_diagnostics(&path);
}
}

/// A wrapper over message receiver that keeps a trace of all received messages and times out
/// long-running requests.
impl AsRef<Fixture> for MockClient {
fn as_ref(&self) -> &Fixture {
&self.fixture
}
}

#[derive(Debug)]
enum RecvError {
Timeout,
NoMessage,
}

impl From<tokio::time::error::Elapsed> for RecvError {
fn from(_: tokio::time::error::Elapsed) -> Self {
RecvError::Timeout
}
}

/// A wrapper over message receiver that keeps a trace of all received messages and timeouts
/// hanging requests.
struct ServerOutput {
output_rx: mpsc::Receiver<Message>,
trace: Vec<Message>,
Expand All @@ -249,13 +314,30 @@ impl ServerOutput {
}

/// Receives a message from the server.
async fn recv(&mut self) -> Result<Option<Message>, Elapsed> {
async fn recv(&mut self) -> Result<Option<Message>, RecvError> {
const TIMEOUT: Duration = Duration::from_secs(2 * 60);
Ok(timeout(TIMEOUT, self.output_rx.next())
.await?
.inspect(|msg| self.trace.push(msg.clone())))
}

/// Looks for a message that satisfies the given predicate in message trace or waits for a new
/// one.
async fn wait_for_message(
&mut self,
predicate: impl Fn(&Message) -> bool,
) -> Result<(), RecvError> {
for message in &self.trace {
if predicate(message) {
return Ok(());
}
}

let r = timeout(TIMEOUT, self.output_rx.next()).await;
if let Ok(Some(msg)) = &r {
self.trace.push(msg.clone());
loop {
let message = self.recv().await?.ok_or(RecvError::NoMessage)?;
if predicate(&message) {
return Ok(());
}
}
r
}
}
1 change: 1 addition & 0 deletions crates/cairo-lang-language-server/tests/e2e/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod client_capabilities;
pub mod fixture;
mod jsonrpc;
pub mod mock_client;
pub mod normalize;
mod runtime;

/// Create a sandboxed environment for testing language server features.
Expand Down
Loading

0 comments on commit d4a3a92

Please sign in to comment.