diff options
Diffstat (limited to 'support/texlab/tests/support/mod.rs')
-rw-r--r-- | support/texlab/tests/support/mod.rs | 635 |
1 files changed, 635 insertions, 0 deletions
diff --git a/support/texlab/tests/support/mod.rs b/support/texlab/tests/support/mod.rs new file mode 100644 index 0000000000..abd1bfe313 --- /dev/null +++ b/support/texlab/tests/support/mod.rs @@ -0,0 +1,635 @@ +use copy_dir::copy_dir; +use futures::lock::Mutex; +use futures_boxed::boxed; +use jsonrpc::client::Result; +use lsp_types::*; +use serde::Serialize; +use std::collections::HashMap; +use std::fs::remove_dir; +use std::path::PathBuf; +use std::sync::Arc; +use tempfile::{tempdir, TempDir}; +use texlab::build::BuildOptions; +use texlab::client::LspClient; +use texlab::diagnostics::LatexLintOptions; +use texlab::formatting::bibtex::BibtexFormattingOptions; +use texlab::server::LatexLspServer; +use texlab::workspace::Uri; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct MockLspClientOptions { + pub bibtex_formatting: Option<BibtexFormattingOptions>, + pub latex_lint: Option<LatexLintOptions>, + pub latex_build: Option<BuildOptions>, +} + +#[derive(Debug, Default)] +pub struct MockLspClient { + pub messages: Mutex<Vec<ShowMessageParams>>, + pub options: Mutex<MockLspClientOptions>, + pub diagnostics_by_uri: Mutex<HashMap<Uri, Vec<Diagnostic>>>, + pub log_messages: Mutex<Vec<LogMessageParams>>, +} + +impl MockLspClient { + pub fn new() -> Self { + Self::default() + } +} + +impl LspClient for MockLspClient { + #[boxed] + async fn configuration(&self, params: ConfigurationParams) -> Result<serde_json::Value> { + fn serialize<T>(options: &Option<T>) -> Result<serde_json::Value> + where + T: Serialize, + { + options + .as_ref() + .map(|options| serde_json::to_value(vec![options]).unwrap()) + .ok_or_else(|| jsonrpc::Error::internal_error("Internal error".to_owned())) + } + + let options = self.options.lock().await; + match params.items[0].section.as_ref().unwrap().as_ref() { + "bibtex.formatting" => serialize(&options.bibtex_formatting), + "latex.lint" => serialize(&options.latex_lint), + "latex.build" => serialize(&options.latex_build), + _ => panic!("Invalid language configuration!"), + } + } + + #[boxed] + async fn show_message(&self, params: ShowMessageParams) { + let mut messages = self.messages.lock().await; + messages.push(params); + } + + #[boxed] + async fn register_capability(&self, _params: RegistrationParams) -> Result<()> { + Ok(()) + } + + #[boxed] + async fn publish_diagnostics(&self, params: PublishDiagnosticsParams) { + let mut diagnostics_by_uri = self.diagnostics_by_uri.lock().await; + diagnostics_by_uri.insert(params.uri.into(), params.diagnostics); + } + + #[boxed] + async fn work_done_progress_create(&self, _params: WorkDoneProgressCreateParams) -> Result<()> { + Ok(()) + } + + #[boxed] + async fn progress(&self, _params: ProgressParams) {} + + #[boxed] + async fn log_message(&self, params: LogMessageParams) { + let mut messages = self.log_messages.lock().await; + messages.push(params); + } +} + +pub struct Scenario { + pub directory: TempDir, + pub server: LatexLspServer<MockLspClient>, + pub client: Arc<MockLspClient>, +} + +impl Scenario { + pub fn new(name: &str, distribution: Arc<Box<dyn tex::Distribution>>) -> Self { + let directory = tempdir().unwrap(); + remove_dir(directory.path()).unwrap(); + let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("scenarios") + .join(name); + copy_dir(source, directory.path()).unwrap(); + + let client = Arc::new(MockLspClient::new()); + let server = LatexLspServer::new(distribution, Arc::clone(&client)); + Self { + directory, + server, + client, + } + } + + pub async fn initialize(&self, capabilities: &ClientCapabilities) { + let root_uri = Uri::from_file_path(self.directory.path()).unwrap(); + let params = InitializeParams { + process_id: None, + root_path: Some(self.directory.path().to_string_lossy().into_owned()), + root_uri: Some(root_uri.into()), + initialization_options: None, + capabilities: capabilities.clone(), + trace: None, + workspace_folders: None, + }; + + self.server + .execute_async(|svr| svr.initialize(params)) + .await + .unwrap(); + + self.server + .execute(|svr| svr.initialized(InitializedParams {})) + .await; + } + + pub fn uri(&self, name: &str) -> Uri { + let mut path = self.directory.path().to_owned(); + path.push(name); + Uri::from_file_path(path).unwrap() + } + + pub async fn read(&self, name: &'static str) -> String { + let mut path = self.directory.path().to_owned(); + path.push(name); + let data = tokio::fs::read(path).await.unwrap(); + let text = String::from_utf8_lossy(&data); + text.replace('\r', "") + } + + pub async fn open(&self, name: &'static str) { + let text = self.read(name).await; + let language_id = if name.ends_with(".bib") { + "bibtex" + } else { + "latex" + }; + + let params = DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: self.uri(name).into(), + version: 0, + language_id: language_id.to_owned(), + text, + }, + }; + self.server.execute(|svr| svr.did_open(params)).await; + } +} + +pub mod capabilities { + use lsp_types::*; + + pub static CLIENT_FULL_CAPABILITIES: ClientCapabilities = ClientCapabilities { + workspace: Some(WorkspaceClientCapabilities { + configuration: Some(true), + did_change_watched_files: None, + workspace_folders: None, + apply_edit: None, + execute_command: None, + symbol: None, + workspace_edit: None, + did_change_configuration: None, + }), + text_document: Some(TextDocumentClientCapabilities { + synchronization: None, + completion: None, + hover: None, + signature_help: None, + references: None, + document_highlight: None, + document_symbol: Some(DocumentSymbolCapability { + dynamic_registration: None, + hierarchical_document_symbol_support: Some(true), + symbol_kind: None, + }), + formatting: None, + range_formatting: None, + on_type_formatting: None, + declaration: None, + definition: Some(GotoCapability { + dynamic_registration: None, + link_support: Some(true), + }), + type_definition: None, + implementation: None, + code_action: None, + code_lens: None, + document_link: None, + color_provider: None, + rename: None, + publish_diagnostics: None, + folding_range: None, + }), + experimental: None, + window: Some(WindowClientCapabilities { + work_done_progress: Some(true), + }), + }; + + pub static CLIENT_NO_LINK_CAPABILITIES: ClientCapabilities = ClientCapabilities { + workspace: Some(WorkspaceClientCapabilities { + configuration: Some(true), + did_change_watched_files: None, + workspace_folders: None, + apply_edit: None, + execute_command: None, + symbol: None, + workspace_edit: None, + did_change_configuration: None, + }), + text_document: Some(TextDocumentClientCapabilities { + synchronization: None, + completion: None, + hover: None, + signature_help: None, + references: None, + document_highlight: None, + document_symbol: None, + formatting: None, + range_formatting: None, + on_type_formatting: None, + declaration: None, + definition: Some(GotoCapability { + dynamic_registration: None, + link_support: Some(false), + }), + type_definition: None, + implementation: None, + code_action: None, + code_lens: None, + document_link: None, + color_provider: None, + rename: None, + publish_diagnostics: None, + folding_range: None, + }), + experimental: None, + window: Some(WindowClientCapabilities { + work_done_progress: Some(true), + }), + }; +} + +pub mod completion { + use super::*; + + pub async fn run_list( + scenario_short_name: &'static str, + file: &'static str, + line: u64, + character: u64, + ) -> (Scenario, Vec<CompletionItem>) { + let scenario_name = format!("completion/{}", scenario_short_name); + let scenario = Scenario::new(&scenario_name, Arc::new(Box::new(tex::Unknown))); + scenario.open(file).await; + scenario + .initialize(&capabilities::CLIENT_FULL_CAPABILITIES) + .await; + + let params = CompletionParams { + text_document_position: TextDocumentPositionParams { + text_document: TextDocumentIdentifier::new(scenario.uri(file).into()), + position: Position::new(line, character), + }, + context: None, + }; + + let items = scenario + .server + .execute_async(|svr| svr.completion(params)) + .await + .unwrap() + .items; + + (scenario, items) + } + + pub async fn run_empty( + scenario_short_name: &'static str, + file: &'static str, + line: u64, + character: u64, + ) { + assert!(run_list(scenario_short_name, file, line, character) + .await + .1 + .is_empty()); + } + + pub async fn run_item( + scenario_short_name: &'static str, + file: &'static str, + line: u64, + character: u64, + item_name: &'static str, + ) -> CompletionItem { + let (scenario, items) = run_list(scenario_short_name, file, line, character).await; + + let item = items + .into_iter() + .find(|item| item.label == item_name) + .unwrap(); + + scenario + .server + .execute_async(|svr| svr.completion_resolve(item)) + .await + .unwrap() + } + + pub mod verify { + use lsp_types::*; + use texlab::range::RangeExt; + + pub fn text_edit( + item: &CompletionItem, + start_line: u64, + start_character: u64, + end_line: u64, + end_character: u64, + text: &str, + ) { + assert_eq!( + item.text_edit, + Some(TextEdit::new( + Range::new_simple(start_line, start_character, end_line, end_character), + text.into() + )) + ); + } + + pub fn detail(item: &CompletionItem, detail: &str) { + assert_eq!(item.detail.as_ref().unwrap(), detail); + } + + pub fn labels(items: &[CompletionItem], expected_labels: Vec<&'static str>) { + let mut actual_labels: Vec<&str> = + items.iter().map(|item| item.label.as_ref()).collect(); + actual_labels.sort(); + assert_eq!(actual_labels, expected_labels); + } + } +} + +pub mod definition { + use super::capabilities::*; + use super::*; + use texlab::definition::DefinitionResponse; + + pub async fn run( + scenario_short_name: &'static str, + file: &'static str, + line: u64, + character: u64, + capabilities: &ClientCapabilities, + ) -> (Scenario, DefinitionResponse) { + let scenario_name = format!("definition/{}", scenario_short_name); + let scenario = Scenario::new(&scenario_name, Arc::new(Box::new(tex::Unknown))); + scenario.initialize(capabilities).await; + scenario.open(file).await; + + let params = TextDocumentPositionParams { + text_document: TextDocumentIdentifier::new(scenario.uri(file).into()), + position: Position::new(line, character), + }; + + let response = scenario + .server + .execute_async(|svr| svr.definition(params)) + .await + .unwrap(); + + (scenario, response) + } + + pub async fn run_link( + scenario_short_name: &'static str, + file: &'static str, + line: u64, + character: u64, + ) -> (Scenario, Vec<LocationLink>) { + let (scenario, response) = run( + scenario_short_name, + file, + line, + character, + &CLIENT_FULL_CAPABILITIES, + ) + .await; + match response { + DefinitionResponse::LocationLinks(links) => (scenario, links), + DefinitionResponse::Locations(_) => unreachable!(), + } + } + + pub async fn run_location( + scenario_short_name: &'static str, + file: &'static str, + line: u64, + character: u64, + ) -> (Scenario, Vec<Location>) { + let (scenario, response) = run( + scenario_short_name, + file, + line, + character, + &CLIENT_NO_LINK_CAPABILITIES, + ) + .await; + match response { + DefinitionResponse::LocationLinks(_) => unreachable!(), + DefinitionResponse::Locations(locations) => (scenario, locations), + } + } + + pub mod verify { + use super::*; + use texlab::range::RangeExt; + + pub fn origin_selection_range( + link: &LocationLink, + start_line: u64, + start_character: u64, + end_line: u64, + end_character: u64, + ) { + assert_eq!( + link.origin_selection_range, + Some(Range::new_simple( + start_line, + start_character, + end_line, + end_character + )) + ); + } + } +} + +pub mod folding { + use super::*; + use std::cmp::Reverse; + + pub async fn run(file: &'static str) -> Vec<FoldingRange> { + let scenario = Scenario::new("folding", Arc::new(Box::new(tex::Unknown))); + scenario + .initialize(&capabilities::CLIENT_FULL_CAPABILITIES) + .await; + scenario.open(file).await; + let params = FoldingRangeParams { + text_document: TextDocumentIdentifier::new(scenario.uri(file).into()), + }; + + let mut foldings = scenario + .server + .execute_async(|svr| svr.folding_range(params)) + .await + .unwrap(); + + foldings.sort_by_key(|folding| { + let start = Position::new(folding.start_line, folding.start_character.unwrap()); + let end = Position::new(folding.end_line, folding.end_character.unwrap()); + (start, Reverse(end)) + }); + foldings + } +} + +pub mod formatting { + use super::*; + use texlab::formatting::bibtex::BibtexFormattingOptions; + + pub async fn run_bibtex( + file: &'static str, + options: Option<BibtexFormattingOptions>, + ) -> (Scenario, Vec<TextEdit>) { + let scenario = Scenario::new("formatting/bibtex", Arc::new(Box::new(tex::Unknown))); + scenario + .initialize(&capabilities::CLIENT_FULL_CAPABILITIES) + .await; + scenario.open(file).await; + { + scenario.client.options.lock().await.bibtex_formatting = options; + } + + let params = DocumentFormattingParams { + text_document: TextDocumentIdentifier::new(scenario.uri(file).into()), + options: FormattingOptions { + tab_size: 4, + insert_spaces: true, + properties: HashMap::new(), + }, + }; + + let edits = scenario + .server + .execute_async(|svr| svr.formatting(params)) + .await + .unwrap(); + (scenario, edits) + } +} + +pub mod hover { + use super::*; + + pub async fn run( + scenario_short_name: &'static str, + file: &'static str, + line: u64, + character: u64, + ) -> Option<HoverContents> { + let scenario_name = format!("hover/{}", scenario_short_name); + let scenario = Scenario::new(&scenario_name, Arc::new(Box::new(tex::Unknown))); + scenario + .initialize(&capabilities::CLIENT_FULL_CAPABILITIES) + .await; + scenario.open(file).await; + let identifier = TextDocumentIdentifier::new(scenario.uri(file).into()); + let params = TextDocumentPositionParams::new(identifier, Position::new(line, character)); + scenario + .server + .execute_async(|svr| svr.hover(params)) + .await + .unwrap() + .map(|hover| hover.contents) + } +} + +pub mod symbol { + use super::*; + use texlab::symbol::SymbolResponse; + + pub async fn run_hierarchical(file: &'static str) -> Vec<DocumentSymbol> { + let scenario = Scenario::new("symbol/hierarchical", Arc::new(Box::new(tex::Unknown))); + scenario + .initialize(&capabilities::CLIENT_FULL_CAPABILITIES) + .await; + scenario.open(file).await; + let params = DocumentSymbolParams { + text_document: TextDocumentIdentifier::new(scenario.uri(file).into()), + }; + + let response = scenario + .server + .execute_async(|svr| svr.document_symbol(params)) + .await + .unwrap(); + + match response { + SymbolResponse::Hierarchical(symbols) => symbols, + SymbolResponse::Flat(_) => unreachable!(), + } + } + + pub async fn run_workspace(query: &'static str) -> (Scenario, Vec<SymbolInformation>) { + let scenario = Scenario::new("symbol/workspace", Arc::new(Box::new(tex::Unknown))); + scenario + .initialize(&capabilities::CLIENT_FULL_CAPABILITIES) + .await; + scenario.open("foo.tex").await; + scenario.open("bar.bib").await; + let params = WorkspaceSymbolParams { + query: query.into(), + }; + let symbols = scenario + .server + .execute_async(|svr| svr.workspace_symbol(params)) + .await + .unwrap(); + + (scenario, symbols) + } + + pub mod verify { + use super::*; + use texlab::range::RangeExt; + + pub fn symbol( + symbol: &DocumentSymbol, + name: &str, + detail: Option<&str>, + selection_range: Range, + range: Range, + ) { + assert_eq!(symbol.name, name); + assert_eq!(symbol.detail.as_ref().map(AsRef::as_ref), detail); + assert_eq!(symbol.selection_range, selection_range); + assert_eq!(symbol.range, range); + } + + pub fn symbol_info( + symbol: &SymbolInformation, + scenario: &Scenario, + file: &str, + name: &str, + start_line: u64, + start_character: u64, + end_line: u64, + end_character: u64, + ) { + assert_eq!(symbol.name, name); + let range = Range::new_simple(start_line, start_character, end_line, end_character); + assert_eq!( + symbol.location, + Location::new(scenario.uri(file).into(), range) + ); + } + } +} |