#![cfg(unix)] #[path = "common/lsp.rs"] #[allow(dead_code)] mod support; use std::fs; use std::path::PathBuf; use std::time::Duration; use serde_json::json; use support::{Server, avril, backend}; fn fixture() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../lib/lsp/tests/fixture") } fn start(backend: &std::path::Path) -> Server { Server::start(&fixture(), backend, &[]) } fn signal(pid: u32, signal: i32) { assert_eq!(unsafe { libc::kill(pid as i32, signal) }, 0); } fn alive(pid: u32) -> bool { let result = unsafe { libc::kill(pid as i32, 0) }; result == 0 } fn native_child(server: &Server) -> u32 { let mut children = Vec::new(); for task in fs::read_dir(format!("/proc/{}/task", server.pid())).unwrap() { let text = fs::read_to_string(task.unwrap().path().join("children")).unwrap_or_default(); children.extend( text.split_whitespace() .map(|pid| pid.parse::().unwrap()), ); } assert_eq!( children.len(), 1, "Expected one native child, found {children:?}" ); children[0] } #[test] fn lifecycle_capabilities_and_native_requests() { let Some(backend) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; let mut server = start(&backend); let hover = server.request("textDocument/hover", json!({})); assert_eq!(hover["error"]["code"], json!(-32002)); let init = server.request("initialize", json!({"capabilities": {}})); assert!(init.get("error").is_none(), "{init}"); let capabilities = &init["result"]["capabilities"]; for feature in ["renameProvider", "codeActionProvider"] { assert!(capabilities.get(feature).is_some(), "{feature}"); } assert_eq!( capabilities["completionProvider"]["resolveProvider"], json!(true) ); assert!(capabilities.get("signatureHelpProvider").is_some()); let main = server.uri("main.tsx"); let text = fs::read_to_string(fixture().join("main.tsx")).unwrap(); server.notify( "textDocument/didOpen", json!({ "textDocument": { "uri": main, "languageId": "typescriptreact", "version": 1, "text": text } }), ); let definitions = server.request( "textDocument/definition", json!({ "textDocument": {"uri": main}, "position": {"line": 4, "character": 24} }), ); assert!(definitions.get("error").is_none(), "{definitions}"); let definitions = definitions["result"].as_array().unwrap(); assert_eq!(definitions.len(), 1); for definition in definitions { assert!(definition.get("uri").is_some() && definition.get("range").is_some()); assert!(definition.get("targetUri").is_none()); } let rename = server.request( "textDocument/rename", json!({ "textDocument": {"uri": main}, "position": support::position_at(&text, text.rfind("localValue").unwrap() + 1), "newName": "renamedValue" }), ); assert!(rename.get("error").is_none(), "{rename}"); assert!( rename["result"]["changes"] .as_object() .is_some_and(|changes| !changes.is_empty()) || rename["result"]["documentChanges"] .as_array() .is_some_and(|changes| !changes.is_empty()), "{rename}" ); let completion = server.request( "textDocument/completion", json!({ "textDocument": {"uri": main}, "position": {"line": 8, "character": 8} }), ); assert!(completion.get("error").is_none(), "{completion}"); let names: Vec<&str> = completion["result"]["items"] .as_array() .unwrap() .iter() .map(|item| item["label"].as_str().unwrap()) .collect(); assert!(names.contains(&"args") && names.contains(&"cwd")); let shutdown = server.request("shutdown", json!({})); assert!(shutdown.get("error").is_none()); let hover = server.request("textDocument/hover", json!({})); assert_eq!(hover["error"]["code"], json!(-32600)); server.notify("exit", json!({})); // Keep stdin open: the server must not wait for its next blocking read. assert!(server.wait_status(Duration::from_secs(8)).success()); } #[test] fn rejects_mismatched_native_backend_versions() { let Some(backend) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; let _ = backend; let mut server = start(&avril()); let init = server.request("initialize", json!({})); assert!( init["error"]["message"] .as_str() .unwrap() .contains("Expected TypeScript 7.1.0-dev.20261013.1 native executable"), "{init}" ); server.notify("exit", json!({})); assert!(!server.wait_status(Duration::from_secs(8)).success()); } #[test] fn terminates_on_malformed_input() { let Some(backend) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; let mut server = start(&backend); server.write_raw(b"Content-Length: 999999999\r\n\r\n"); assert!(!server.wait_status(Duration::from_secs(8)).success()); } #[test] #[cfg(target_os = "linux")] fn cancellation_and_native_child_cleanup() { let Some(backend) = backend() else { eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set"); return; }; for mode in [ "eof", "interrupt", "terminate", "exit", "crash", "hang", "cancel", "shutdown-interrupt", ] { let mut server = start(&backend); let init = server.request("initialize", json!({})); assert!(init.get("error").is_none(), "{mode}: {init}"); let main = server.uri("main.tsx"); let text = fs::read_to_string(fixture().join("main.tsx")).unwrap(); server.notify( "textDocument/didOpen", json!({ "textDocument": { "uri": main, "languageId": "typescriptreact", "version": 1, "text": text } }), ); let hover = server.request( "textDocument/hover", json!({ "textDocument": {"uri": main}, "position": {"line": 4, "character": 24} }), ); assert!(hover.get("error").is_none(), "{mode}: {hover}"); // Tokio may spawn the backend on either runtime worker. let native = native_child(&server); match mode { "eof" => server.close_stdin(), "interrupt" => signal(server.pid(), libc::SIGINT), "terminate" => signal(server.pid(), libc::SIGTERM), "exit" => server.notify("exit", json!({})), "hang" => { signal(native, libc::SIGSTOP); let shutdown = server.request("shutdown", json!({})); assert!( shutdown.get("error").is_some(), "Hung backend must time out" ); server.notify("exit", json!({})); } "shutdown-interrupt" => { signal(native, libc::SIGSTOP); let shutdown = server.request_start("shutdown", json!({})); std::thread::sleep(Duration::from_millis(1500)); signal(server.pid(), libc::SIGTERM); assert!(!server.wait_status(Duration::from_secs(8)).success()); // The in-flight shutdown request can never be answered now; // dropping the server below reaps the child. let _ = shutdown; } "cancel" => { signal(native, libc::SIGSTOP); let id = server.request_start( "textDocument/hover", json!({ "textDocument": {"uri": main}, "position": {"line": 4, "character": 24} }), ); server.notify("$/cancelRequest", json!({"id": id})); std::thread::sleep(Duration::from_millis(50)); signal(native, libc::SIGCONT); let response = server.request_wait(id); assert_eq!(response["error"]["code"], json!(-32800), "{mode}"); let shutdown = server.request("shutdown", json!({})); assert!(shutdown.get("error").is_none()); server.notify("exit", json!({})); } _ => { use std::os::fd::AsRawFd; use std::time::Instant; // New requests recover an already failed backend. Kill it only // after this request has reached the stopped backend's pipe. signal(native, libc::SIGSTOP); let stdin = fs::File::open(format!("/proc/{native}/fd/0")).unwrap(); let params = json!({ "textDocument": {"uri": main}, "position": {"line": 4, "character": 24} }); let id = server.request_start("textDocument/hover", params.clone()); let deadline = Instant::now() + Duration::from_secs(15); loop { let mut bytes: libc::c_int = 0; assert_eq!( unsafe { libc::ioctl(stdin.as_raw_fd(), libc::FIONREAD, &mut bytes) }, 0 ); if bytes > 0 { break; } if Instant::now() >= deadline { signal(native, libc::SIGKILL); panic!("Hover did not reach the native backend"); } std::thread::sleep(Duration::from_millis(10)); } drop(stdin); signal(native, libc::SIGKILL); let hover = server.request_wait(id); assert!( hover.get("error").is_some(), "Backend failure must fail pending editor requests: {hover}" ); let recovered = server.request("textDocument/hover", params); assert!(recovered["result"].is_object(), "{recovered}"); let replacement = native_child(&server); assert_ne!(replacement, native); server.request("shutdown", json!({})); server.notify("exit", json!({})); assert!(server.wait_status(Duration::from_secs(8)).success()); assert!(!alive(replacement)); } } if mode != "shutdown-interrupt" { server.wait_status(Duration::from_secs(8)); } assert!( !alive(native), "Native process {native} leaked after {mode}" ); } }