erg/crates/els/tests/test.rs
2024-06-16 21:12:53 +09:00

442 lines
16 KiB
Rust

use std::path::Path;
use erg_common::spawn::safe_yield;
use lsp_types::{
CompletionResponse, DiagnosticSeverity, DocumentSymbolResponse, FoldingRange, FoldingRangeKind,
GotoDefinitionResponse, HoverContents, InlayHintLabel, MarkedString,
};
const FILE_A: &str = "tests/a.er";
const FILE_B: &str = "tests/b.er";
const FILE_C: &str = "tests/c.er";
const FILE_IMPORTS: &str = "tests/imports.er";
const FILE_INVALID_SYNTAX: &str = "tests/invalid_syntax.er";
const FILE_RETRIGGER: &str = "tests/retrigger.er";
const FILE_TOLERANT_COMPLETION: &str = "tests/tolerant_completion.er";
use els::{NormalizedUrl, Server};
use erg_proc_macros::exec_new_thread;
use molc::{add_char, delete_line, oneline_range};
#[test]
fn test_open() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
client.wait_messages(3)?;
client.notify_open(FILE_A)?;
// log, work-done, etc.
client.wait_messages(6)?;
assert!(client.responses.iter().any(|val| val
.to_string()
.contains("tests/a.er passed, found warns: 0")));
Ok(())
}
#[test]
fn test_completion() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri_a = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
client.notify_open(FILE_A)?;
client.notify_change(uri_a.clone().raw(), add_char(2, 0, "x"))?;
client.notify_change(uri_a.clone().raw(), add_char(2, 1, "."))?;
let resp = client.request_completion(uri_a.raw(), 2, 2, ".")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 40);
assert!(items.iter().any(|item| item.label == "abs"));
} else {
return Err(format!("{}: not items: {resp:?}", line!()).into());
}
client.notify_open(FILE_B)?;
client.notify_change(uri_b.clone().raw(), add_char(6, 20, "."))?;
let resp = client.request_completion(uri_b.raw(), 6, 21, ".")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.iter().any(|item| item.label == "a"));
} else {
return Err(format!("{}: not items: {resp:?}", line!()).into());
}
Ok(())
}
#[test]
fn test_completion_item_resolve() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri_a = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
client.notify_change(uri_a.clone().raw(), add_char(2, 0, "x"))?;
client.notify_change(uri_a.clone().raw(), add_char(2, 1, "."))?;
let resp = client.request_completion(uri_a.clone().raw(), 2, 2, ".")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 40);
assert!(items.iter().any(|item| item.label == "abs"));
let Some(item) = items.into_iter().find(|item| item.label == "bit_count") else {
return Err("`bit_count` method not found".into());
};
assert!(item.documentation.is_none());
let resolved = client.request_completion_item_resolve(item)?;
assert_eq!(resolved.label, "bit_count");
assert!(resolved.documentation.is_some());
} else {
return Err(format!("not items: {resp:?}").into());
}
Ok(())
}
#[test]
fn test_neighbor_completion() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
client.notify_open(FILE_B)?;
let resp = client.request_completion(uri.raw(), 2, 0, "n")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 40);
assert!(items
.iter()
.any(|item| item.label == "neighbor (import from b)"));
Ok(())
} else {
Err(format!("not items: {resp:?}").into())
}
}
#[test]
fn test_pymodule_completion() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
while !client.server.flags.builtin_modules_loaded() {
safe_yield();
}
client.notify_change(uri.clone().raw(), add_char(2, 0, "c"))?;
let resp = client.request_completion(uri.raw(), 2, 0, "c")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 100);
assert!(items
.iter()
.any(|item| item.label == "cos (import from math)"));
Ok(())
} else {
Err(format!("not items: {resp:?}").into())
}
}
#[test]
fn test_completion_retrigger() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_RETRIGGER).canonicalize()?)?;
client.notify_open(FILE_RETRIGGER)?;
let _ = client.wait_diagnostics()?;
client.notify_change(uri.clone().raw(), add_char(2, 7, "n"))?;
let resp = client.request_completion(uri.clone().raw(), 2, 7, "n")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(!items.is_empty());
assert!(items.iter().any(|item| item.label == "print!"));
} else {
return Err(format!("{}: not items: {resp:?}", line!()).into());
}
client.notify_change(uri.clone().raw(), add_char(3, 15, "t"))?;
let resp = client.request_completion(uri.raw(), 3, 15, "t")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(!items.is_empty());
assert!(items.iter().any(|item| item.label == "bit_count"));
assert!(items.iter().any(|item| item.label == "bit_length"));
} else {
return Err(format!("{}: not items: {resp:?}", line!()).into());
}
Ok(())
}
#[test]
fn test_tolerant_completion() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_TOLERANT_COMPLETION).canonicalize()?)?;
client.notify_open(FILE_TOLERANT_COMPLETION)?;
let _ = client.wait_diagnostics()?;
client.notify_change(uri.clone().raw(), add_char(5, 16, "."))?;
let resp = client.request_completion(uri.clone().raw(), 5, 17, ".")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 40);
assert!(items.iter().any(|item| item.label == "capitalize"));
} else {
return Err(format!("{}: not items: {resp:?}", line!()).into());
}
client.notify_change(uri.clone().raw(), add_char(5, 14, "."))?;
let resp = client.request_completion(uri.clone().raw(), 5, 15, ".")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 40);
assert!(items.iter().any(|item| item.label == "abs"));
} else {
return Err(format!("{}: not items: {resp:?}", line!()).into());
}
client.notify_change(uri.clone().raw(), add_char(2, 9, "."))?;
let resp = client.request_completion(uri.raw(), 2, 10, ".")?;
if let Some(CompletionResponse::Array(items)) = resp {
assert!(items.len() >= 10);
assert!(items.iter().any(|item| item.label == "pi"));
Ok(())
} else {
Err(format!("{}: not items: {resp:?}", line!()).into())
}
}
#[test]
#[exec_new_thread]
fn test_rename() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let edit = client
.request_rename(uri.clone().raw(), 1, 5, "y")?
.unwrap();
assert!(edit
.changes
.is_some_and(|changes| changes.values().next().unwrap().len() == 2));
client.notify_open(FILE_C)?;
client.notify_open(FILE_B)?;
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
// let uri_c = NormalizedUrl::from_file_path(Path::new(FILE_C).canonicalize()?)?;
let edit = client
.request_rename(uri_b.clone().raw(), 2, 1, "y")?
.unwrap();
assert_eq!(edit.changes.as_ref().unwrap().iter().count(), 2);
for (_, change) in edit.changes.unwrap() {
assert_eq!(change.len(), 1);
}
client.notify_save(uri_b.clone().raw())?;
client.wait_diagnostics()?;
let edit = client
.request_rename(uri_b.clone().raw(), 4, 14, "b")?
.unwrap();
assert_eq!(edit.changes.as_ref().unwrap().iter().count(), 2);
for (uri, change) in edit.changes.unwrap() {
if uri.as_str().ends_with("b.er") {
assert_eq!(change.len(), 2);
} else {
assert_eq!(change.len(), 1); // c.er
}
}
Ok(())
}
#[test]
fn test_signature_help() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
client.notify_change(uri.clone().raw(), add_char(2, 0, "assert"))?;
client.notify_change(uri.clone().raw(), add_char(2, 6, "("))?;
let help = client
.request_signature_help(uri.raw(), 2, 7, "(")?
.unwrap();
assert_eq!(help.signatures.len(), 1);
let sig = &help.signatures[0];
assert_eq!(sig.label, "::assert: (test: Bool, msg := Str) -> NoneType");
assert_eq!(sig.active_parameter, Some(0));
Ok(())
}
#[test]
fn test_hover() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let hover = client.request_hover(uri.raw(), 1, 4)?.unwrap();
let HoverContents::Array(contents) = hover.contents else {
todo!()
};
assert_eq!(contents.len(), 2);
let MarkedString::LanguageString(content) = &contents[0] else {
todo!()
};
assert!(
content.value == "# tests/a.er, line 1\nx = 1"
|| content.value == "# tests\\a.er, line 1\nx = 1"
);
let MarkedString::LanguageString(content) = &contents[1] else {
todo!()
};
assert_eq!(content.value, "x: {1}");
Ok(())
}
#[test]
#[exec_new_thread]
fn test_references() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let locations = client.request_references(uri.raw(), 1, 4)?.unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(&locations[0].range, &oneline_range(1, 4, 5));
client.notify_open(FILE_C)?;
client.notify_open(FILE_B)?;
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
let uri_c = NormalizedUrl::from_file_path(Path::new(FILE_C).canonicalize()?)?;
let locations = client.request_references(uri_b.raw(), 0, 2)?.unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(NormalizedUrl::new(locations[0].uri.clone()), uri_c);
Ok(())
}
#[test]
fn test_goto_definition() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let Some(GotoDefinitionResponse::Scalar(location)) =
client.request_goto_definition(uri.raw(), 1, 4)?
else {
todo!()
};
assert_eq!(&location.range, &oneline_range(0, 0, 1));
Ok(())
}
#[test]
#[exec_new_thread]
fn test_folding_range() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_IMPORTS).canonicalize()?)?;
client.notify_open(FILE_IMPORTS)?;
let ranges = client.request_folding_range(uri.raw())?.unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(
&ranges[0],
&FoldingRange {
start_line: 0,
start_character: Some(0),
end_line: 3,
end_character: Some(22),
kind: Some(FoldingRangeKind::Imports),
}
);
Ok(())
}
#[test]
fn test_document_symbol() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let Some(DocumentSymbolResponse::Nested(symbols)) =
client.request_document_symbols(uri.raw())?
else {
todo!()
};
assert_eq!(symbols.len(), 2);
assert_eq!(&symbols[0].name, "x");
Ok(())
}
#[test]
fn test_inlay_hint() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
client.notify_open(FILE_A)?;
let hints = client.request_inlay_hint(uri.raw())?.unwrap();
assert_eq!(hints.len(), 2);
let InlayHintLabel::String(label) = &hints[0].label else {
todo!()
};
assert_eq!(label, ": {1}");
let InlayHintLabel::String(label) = &hints[1].label else {
todo!()
};
assert_eq!(label, ": Nat");
Ok(())
}
#[test]
#[exec_new_thread]
fn test_dependents_check() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
client.wait_messages(3)?;
client.notify_open(FILE_B)?;
client.wait_messages(6)?;
client.notify_open(FILE_C)?;
client.wait_messages(6)?;
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
// delete b.er:3, causing an error in c.er
client.notify_change(uri_b.clone().raw(), delete_line(2))?;
client.wait_messages(2)?;
client.responses.clear();
client.notify_save(uri_b.clone().raw())?;
let b_diags = client.wait_diagnostics()?;
assert!(b_diags.diagnostics.is_empty(), "{:?}", b_diags.diagnostics);
let c_diags = client.wait_diagnostics()?;
assert_eq!(
c_diags.diagnostics.len(),
1,
"{:?} / {:?}",
b_diags.diagnostics,
c_diags.diagnostics
);
assert_eq!(
c_diags.diagnostics[0].severity,
Some(DiagnosticSeverity::ERROR)
);
// insert invalid code to b.er:8, causing a syntax error in b.er but not c.er
client.notify_change(uri_b.clone().raw(), add_char(7, 0, "a.\n"))?;
client.wait_messages(1)?;
client.responses.clear();
client.notify_save(uri_b.clone().raw())?;
let b_diags = client.wait_diagnostics()?;
assert_eq!(b_diags.diagnostics.len(), 1, "{:?}", b_diags.diagnostics,);
let c_diags = client.wait_diagnostics()?;
assert!(c_diags
.diagnostics
.iter()
.all(|diag| !diag.message.contains("expected: Indent, got: EOF")));
Ok(())
}
#[test]
fn test_fix_error() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Server::bind_fake_client();
client.request_initialize()?;
client.notify_initialized()?;
client.notify_open(FILE_INVALID_SYNTAX)?;
let diags = client.wait_diagnostics()?;
assert_eq!(diags.diagnostics.len(), 1);
assert_eq!(
diags.diagnostics[0].severity,
Some(DiagnosticSeverity::ERROR)
);
let uri = NormalizedUrl::from_file_path(Path::new(FILE_INVALID_SYNTAX).canonicalize()?)?;
client.notify_change(uri.clone().raw(), add_char(0, 10, " 1"))?;
client.notify_save(uri.clone().raw())?;
let diags = client.wait_diagnostics()?;
assert_eq!(diags.diagnostics.len(), 0);
Ok(())
}