char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somclean up tests and clippy4d5ee6a

main
10.8 KiB302 linesraw
1#![cfg(unix)]
2
3#[path = "common/lsp.rs"]
4#[allow(dead_code)]
5mod support;
6
7use std::fs;
8use std::path::PathBuf;
9use std::time::Duration;
10
11use serde_json::json;
12use support::{Server, avril, backend};
13
14fn fixture() -> PathBuf {
15    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../lib/lsp/tests/fixture")
16}
17
18fn start(backend: &std::path::Path) -> Server {
19    Server::start(&fixture(), backend, &[])
20}
21
22fn signal(pid: u32, signal: i32) {
23    assert_eq!(unsafe { libc::kill(pid as i32, signal) }, 0);
24}
25
26fn alive(pid: u32) -> bool {
27    let result = unsafe { libc::kill(pid as i32, 0) };
28    result == 0
29}
30
31fn native_child(server: &Server) -> u32 {
32    let mut children = Vec::new();
33    for task in fs::read_dir(format!("/proc/{}/task", server.pid())).unwrap() {
34        let text = fs::read_to_string(task.unwrap().path().join("children")).unwrap_or_default();
35        children.extend(
36            text.split_whitespace()
37                .map(|pid| pid.parse::<u32>().unwrap()),
38        );
39    }
40    assert_eq!(
41        children.len(),
42        1,
43        "Expected one native child, found {children:?}"
44    );
45    children[0]
46}
47
48#[test]
49fn lifecycle_capabilities_and_native_requests() {
50    let Some(backend) = backend() else {
51        eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set");
52        return;
53    };
54    let mut server = start(&backend);
55    let hover = server.request("textDocument/hover", json!({}));
56    assert_eq!(hover["error"]["code"], json!(-32002));
57    let init = server.request("initialize", json!({"capabilities": {}}));
58    assert!(init.get("error").is_none(), "{init}");
59    let capabilities = &init["result"]["capabilities"];
60    for feature in ["renameProvider", "codeActionProvider"] {
61        assert!(capabilities.get(feature).is_some(), "{feature}");
62    }
63    assert_eq!(
64        capabilities["completionProvider"]["resolveProvider"],
65        json!(true)
66    );
67    assert!(capabilities.get("signatureHelpProvider").is_some());
68
69    let main = server.uri("main.tsx");
70    let text = fs::read_to_string(fixture().join("main.tsx")).unwrap();
71    server.notify(
72        "textDocument/didOpen",
73        json!({
74            "textDocument": {
75                "uri": main, "languageId": "typescriptreact", "version": 1, "text": text
76            }
77        }),
78    );
79    let definitions = server.request(
80        "textDocument/definition",
81        json!({
82            "textDocument": {"uri": main},
83            "position": {"line": 4, "character": 24}
84        }),
85    );
86    assert!(definitions.get("error").is_none(), "{definitions}");
87    let definitions = definitions["result"].as_array().unwrap();
88    assert_eq!(definitions.len(), 1);
89    for definition in definitions {
90        assert!(definition.get("uri").is_some() && definition.get("range").is_some());
91        assert!(definition.get("targetUri").is_none());
92    }
93    let rename = server.request(
94        "textDocument/rename",
95        json!({
96            "textDocument": {"uri": main},
97            "position": support::position_at(&text, text.rfind("localValue").unwrap() + 1),
98            "newName": "renamedValue"
99        }),
100    );
101    assert!(rename.get("error").is_none(), "{rename}");
102    assert!(
103        rename["result"]["changes"]
104            .as_object()
105            .is_some_and(|changes| !changes.is_empty())
106            || rename["result"]["documentChanges"]
107                .as_array()
108                .is_some_and(|changes| !changes.is_empty()),
109        "{rename}"
110    );
111    let completion = server.request(
112        "textDocument/completion",
113        json!({
114            "textDocument": {"uri": main},
115            "position": {"line": 8, "character": 8}
116        }),
117    );
118    assert!(completion.get("error").is_none(), "{completion}");
119    let names: Vec<&str> = completion["result"]["items"]
120        .as_array()
121        .unwrap()
122        .iter()
123        .map(|item| item["label"].as_str().unwrap())
124        .collect();
125    assert!(names.contains(&"args") && names.contains(&"cwd"));
126    let shutdown = server.request("shutdown", json!({}));
127    assert!(shutdown.get("error").is_none());
128    let hover = server.request("textDocument/hover", json!({}));
129    assert_eq!(hover["error"]["code"], json!(-32600));
130    server.notify("exit", json!({}));
131    // Keep stdin open: the server must not wait for its next blocking read.
132    assert!(server.wait_status(Duration::from_secs(8)).success());
133}
134
135#[test]
136fn rejects_mismatched_native_backend_versions() {
137    let Some(backend) = backend() else {
138        eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set");
139        return;
140    };
141    let _ = backend;
142    let mut server = start(&avril());
143    let init = server.request("initialize", json!({}));
144    assert!(
145        init["error"]["message"]
146            .as_str()
147            .unwrap()
148            .contains("Expected TypeScript 7.1.0-dev.20261013.1 native executable"),
149        "{init}"
150    );
151    server.notify("exit", json!({}));
152    assert!(!server.wait_status(Duration::from_secs(8)).success());
153}
154
155#[test]
156fn terminates_on_malformed_input() {
157    let Some(backend) = backend() else {
158        eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set");
159        return;
160    };
161    let mut server = start(&backend);
162    server.write_raw(b"Content-Length: 999999999\r\n\r\n");
163    assert!(!server.wait_status(Duration::from_secs(8)).success());
164}
165
166#[test]
167#[cfg(target_os = "linux")]
168fn cancellation_and_native_child_cleanup() {
169    let Some(backend) = backend() else {
170        eprintln!("skipping: AVRIL_TEST_TYPESCRIPT is not set");
171        return;
172    };
173    for mode in [
174        "eof",
175        "interrupt",
176        "terminate",
177        "exit",
178        "crash",
179        "hang",
180        "cancel",
181        "shutdown-interrupt",
182    ] {
183        let mut server = start(&backend);
184        let init = server.request("initialize", json!({}));
185        assert!(init.get("error").is_none(), "{mode}: {init}");
186        let main = server.uri("main.tsx");
187        let text = fs::read_to_string(fixture().join("main.tsx")).unwrap();
188        server.notify(
189            "textDocument/didOpen",
190            json!({
191                "textDocument": {
192                    "uri": main, "languageId": "typescriptreact", "version": 1, "text": text
193                }
194            }),
195        );
196        let hover = server.request(
197            "textDocument/hover",
198            json!({
199                "textDocument": {"uri": main},
200                "position": {"line": 4, "character": 24}
201            }),
202        );
203        assert!(hover.get("error").is_none(), "{mode}: {hover}");
204        // Tokio may spawn the backend on either runtime worker.
205        let native = native_child(&server);
206        match mode {
207            "eof" => server.close_stdin(),
208            "interrupt" => signal(server.pid(), libc::SIGINT),
209            "terminate" => signal(server.pid(), libc::SIGTERM),
210            "exit" => server.notify("exit", json!({})),
211            "hang" => {
212                signal(native, libc::SIGSTOP);
213                let shutdown = server.request("shutdown", json!({}));
214                assert!(
215                    shutdown.get("error").is_some(),
216                    "Hung backend must time out"
217                );
218                server.notify("exit", json!({}));
219            }
220            "shutdown-interrupt" => {
221                signal(native, libc::SIGSTOP);
222                let shutdown = server.request_start("shutdown", json!({}));
223                std::thread::sleep(Duration::from_millis(1500));
224                signal(server.pid(), libc::SIGTERM);
225                assert!(!server.wait_status(Duration::from_secs(8)).success());
226                // The in-flight shutdown request can never be answered now;
227                // dropping the server below reaps the child.
228                let _ = shutdown;
229            }
230            "cancel" => {
231                signal(native, libc::SIGSTOP);
232                let id = server.request_start(
233                    "textDocument/hover",
234                    json!({
235                        "textDocument": {"uri": main},
236                        "position": {"line": 4, "character": 24}
237                    }),
238                );
239                server.notify("$/cancelRequest", json!({"id": id}));
240                std::thread::sleep(Duration::from_millis(50));
241                signal(native, libc::SIGCONT);
242                let response = server.request_wait(id);
243                assert_eq!(response["error"]["code"], json!(-32800), "{mode}");
244                let shutdown = server.request("shutdown", json!({}));
245                assert!(shutdown.get("error").is_none());
246                server.notify("exit", json!({}));
247            }
248            _ => {
249                use std::os::fd::AsRawFd;
250                use std::time::Instant;
251
252                // New requests recover an already failed backend. Kill it only
253                // after this request has reached the stopped backend's pipe.
254                signal(native, libc::SIGSTOP);
255                let stdin = fs::File::open(format!("/proc/{native}/fd/0")).unwrap();
256                let params = json!({
257                    "textDocument": {"uri": main},
258                    "position": {"line": 4, "character": 24}
259                });
260                let id = server.request_start("textDocument/hover", params.clone());
261                let deadline = Instant::now() + Duration::from_secs(15);
262                loop {
263                    let mut bytes: libc::c_int = 0;
264                    assert_eq!(
265                        unsafe { libc::ioctl(stdin.as_raw_fd(), libc::FIONREAD, &mut bytes) },
266                        0
267                    );
268                    if bytes > 0 {
269                        break;
270                    }
271                    if Instant::now() >= deadline {
272                        signal(native, libc::SIGKILL);
273                        panic!("Hover did not reach the native backend");
274                    }
275                    std::thread::sleep(Duration::from_millis(10));
276                }
277                drop(stdin);
278                signal(native, libc::SIGKILL);
279                let hover = server.request_wait(id);
280                assert!(
281                    hover.get("error").is_some(),
282                    "Backend failure must fail pending editor requests: {hover}"
283                );
284                let recovered = server.request("textDocument/hover", params);
285                assert!(recovered["result"].is_object(), "{recovered}");
286                let replacement = native_child(&server);
287                assert_ne!(replacement, native);
288                server.request("shutdown", json!({}));
289                server.notify("exit", json!({}));
290                assert!(server.wait_status(Duration::from_secs(8)).success());
291                assert!(!alive(replacement));
292            }
293        }
294        if mode != "shutdown-interrupt" {
295            server.wait_status(Duration::from_secs(8));
296        }
297        assert!(
298            !alive(native),
299            "Native process {native} leaked after {mode}"
300        );
301    }
302}