char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
4d5ee6a
main
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 { 15PathBuf :: from ( env! ( "CARGO_MANIFEST_DIR" )). join ( "../lib/lsp/tests/fixture" ) 16} 17 18fn start ( backend : & std:: path:: Path ) ->Server { 19Server :: start ( & fixture (), backend, & []) 20} 21 22fn signal ( pid : u32 , signal : i32 ) { 23assert_eq! ( unsafe { libc:: kill ( pidas i32 , signal) }, 0 ); 24} 25 26fn alive ( pid : u32 ) ->bool { 27let result =unsafe { libc:: kill ( pidas i32 , 0 ) }; 28 result ==0 29} 30 31fn native_child ( server : & Server ) ->u32 { 32let mut children =Vec :: new (); 33for taskin fs:: read_dir ( format! ( "/proc/{}/task" , server. pid ())). unwrap () { 34let 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} 40assert_eq! ( 41 children. len (), 421 , 43"Expected one native child, found {children:?}" 44); 45 children[ 0 ] 46} 47 48# [ test ] 49fn lifecycle_capabilities_and_native_requests () { 50let Some ( backend) =backend () else { 51eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 52return ; 53}; 54let mut server =start ( & backend); 55let hover = server. request ( "textDocument/hover" , json! ({})); 56assert_eq! ( hover[ "error" ][ "code" ], json! ( -32002 )); 57let init = server. request ( "initialize" , json! ({ "capabilities" : {}})); 58assert! ( init. get ( "error" ). is_none (), "{init}" ); 59let capabilities =& init[ "result" ][ "capabilities" ]; 60for featurein [ "renameProvider" , "codeActionProvider" ] { 61assert! ( capabilities. get ( feature). is_some (), "{feature}" ); 62} 63assert_eq! ( 64 capabilities[ "completionProvider" ][ "resolveProvider" ], 65json! ( true ) 66); 67assert! ( capabilities. get ( "signatureHelpProvider" ). is_some ()); 68 69let main = server. uri ( "main.tsx" ); 70let text = fs:: read_to_string ( fixture (). join ( "main.tsx" )). unwrap (); 71 server. notify ( 72"textDocument/didOpen" , 73json! ({ 74"textDocument" : { 75"uri" : main, "languageId" : "typescriptreact" , "version" : 1 , "text" : text 76} 77}), 78); 79let definitions = server. request ( 80"textDocument/definition" , 81json! ({ 82"textDocument" : { "uri" : main}, 83"position" : { "line" : 4 , "character" : 24 } 84}), 85); 86assert! ( definitions. get ( "error" ). is_none (), "{definitions}" ); 87let definitions = definitions[ "result" ]. as_array (). unwrap (); 88assert_eq! ( definitions. len (), 1 ); 89for definitionin definitions{ 90assert! ( definition. get ( "uri" ). is_some () && definition. get ( "range" ). is_some ()); 91assert! ( definition. get ( "targetUri" ). is_none ()); 92} 93let rename = server. request ( 94"textDocument/rename" , 95json! ({ 96"textDocument" : { "uri" : main}, 97"position" : support:: position_at ( & text, text. rfind ( "localValue" ). unwrap () +1 ), 98"newName" : "renamedValue" 99}), 100); 101assert! ( rename. get ( "error" ). is_none (), "{rename}" ); 102assert! ( 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); 111let completion = server. request ( 112"textDocument/completion" , 113json! ({ 114"textDocument" : { "uri" : main}, 115"position" : { "line" : 8 , "character" : 8 } 116}), 117); 118assert! ( completion. get ( "error" ). is_none (), "{completion}" ); 119let names: Vec < & str > = completion[ "result" ][ "items" ] 120. as_array () 121. unwrap () 122. iter () 123. map ( |item| item[ "label" ]. as_str (). unwrap ()) 124. collect (); 125assert! ( names. contains ( & "args" ) && names. contains ( & "cwd" )); 126let shutdown = server. request ( "shutdown" , json! ({})); 127assert! ( shutdown. get ( "error" ). is_none ()); 128let hover = server. request ( "textDocument/hover" , json! ({})); 129assert_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. 132assert! ( server. wait_status ( Duration :: from_secs ( 8 )). success ()); 133} 134 135# [ test ] 136fn rejects_mismatched_native_backend_versions () { 137let Some ( backend) =backend () else { 138eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 139return ; 140}; 141let _ = backend; 142let mut server =start ( & avril ()); 143let init = server. request ( "initialize" , json! ({})); 144assert! ( 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! ({})); 152assert! ( !server. wait_status ( Duration :: from_secs ( 8 )). success ()); 153} 154 155# [ test ] 156fn terminates_on_malformed_input () { 157let Some ( backend) =backend () else { 158eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 159return ; 160}; 161let mut server =start ( & backend); 162 server. write_raw ( b"Content-Length: 999999999\r\n\r\n" ); 163assert! ( !server. wait_status ( Duration :: from_secs ( 8 )). success ()); 164} 165 166# [ test ] 167# [ cfg ( target_os = "linux" )] 168fn cancellation_and_native_child_cleanup () { 169let Some ( backend) =backend () else { 170eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 171return ; 172}; 173for modein [ 174"eof" , 175"interrupt" , 176"terminate" , 177"exit" , 178"crash" , 179"hang" , 180"cancel" , 181"shutdown-interrupt" , 182] { 183let mut server =start ( & backend); 184let init = server. request ( "initialize" , json! ({})); 185assert! ( init. get ( "error" ). is_none (), "{mode}: {init}" ); 186let main = server. uri ( "main.tsx" ); 187let text = fs:: read_to_string ( fixture (). join ( "main.tsx" )). unwrap (); 188 server. notify ( 189"textDocument/didOpen" , 190json! ({ 191"textDocument" : { 192"uri" : main, "languageId" : "typescriptreact" , "version" : 1 , "text" : text 193} 194}), 195); 196let hover = server. request ( 197"textDocument/hover" , 198json! ({ 199"textDocument" : { "uri" : main}, 200"position" : { "line" : 4 , "character" : 24 } 201}), 202); 203assert! ( hover. get ( "error" ). is_none (), "{mode}: {hover}" ); 204// Tokio may spawn the backend on either runtime worker. 205let native =native_child ( & server); 206match 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" =>{ 212signal ( native, libc:: SIGSTOP ); 213let shutdown = server. request ( "shutdown" , json! ({})); 214assert! ( 215 shutdown. get ( "error" ). is_some (), 216"Hung backend must time out" 217); 218 server. notify ( "exit" , json! ({})); 219} 220"shutdown-interrupt" =>{ 221signal ( native, libc:: SIGSTOP ); 222let shutdown = server. request_start ( "shutdown" , json! ({})); 223 std:: thread:: sleep ( Duration :: from_millis ( 1500 )); 224signal ( server. pid (), libc:: SIGTERM ); 225assert! ( !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. 228let _ = shutdown; 229} 230"cancel" =>{ 231signal ( native, libc:: SIGSTOP ); 232let id = server. request_start ( 233"textDocument/hover" , 234json! ({ 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 )); 241signal ( native, libc:: SIGCONT ); 242let response = server. request_wait ( id); 243assert_eq! ( response[ "error" ][ "code" ], json! ( -32800 ), "{mode}" ); 244let shutdown = server. request ( "shutdown" , json! ({})); 245assert! ( shutdown. get ( "error" ). is_none ()); 246 server. notify ( "exit" , json! ({})); 247} 248 _ =>{ 249use std:: os:: fd:: AsRawFd ; 250use 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. 254signal ( native, libc:: SIGSTOP ); 255let stdin = fs:: File :: open ( format! ( "/proc/{native}/fd/0" )). unwrap (); 256let params =json! ({ 257"textDocument" : { "uri" : main}, 258"position" : { "line" : 4 , "character" : 24 } 259}); 260let id = server. request_start ( "textDocument/hover" , params. clone ()); 261let deadline =Instant :: now () +Duration :: from_secs ( 15 ); 262loop { 263let mut bytes: libc:: c_int =0 ; 264assert_eq! ( 265unsafe { libc:: ioctl ( stdin. as_raw_fd (), libc:: FIONREAD , & mut bytes) }, 2660 267); 268if bytes >0 { 269break ; 270} 271if Instant :: now () >= deadline{ 272signal ( native, libc:: SIGKILL ); 273panic! ( "Hover did not reach the native backend" ); 274} 275 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 276} 277drop ( stdin); 278signal ( native, libc:: SIGKILL ); 279let hover = server. request_wait ( id); 280assert! ( 281 hover. get ( "error" ). is_some (), 282"Backend failure must fail pending editor requests: {hover}" 283); 284let recovered = server. request ( "textDocument/hover" , params); 285assert! ( recovered[ "result" ]. is_object (), "{recovered}" ); 286let replacement =native_child ( & server); 287assert_ne! ( replacement, native); 288 server. request ( "shutdown" , json! ({})); 289 server. notify ( "exit" , json! ({})); 290assert! ( server. wait_status ( Duration :: from_secs ( 8 )). success ()); 291assert! ( !alive ( replacement)); 292} 293} 294if mode !="shutdown-interrupt" { 295 server. wait_status ( Duration :: from_secs ( 8 )); 296} 297assert! ( 298 !alive ( native), 299"Native process {native} leaked after {mode}" 300); 301} 302}