123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222openCoreopenAsyncopenImportopenVcamlopenVcaml_test_helpers[%%import"config_ext.h"][%%ifdefinedJSC_LINUX_EXT&&definedJSC_UNIX_PTY]letposix_openpt=ok_exnUnix_pseudo_terminal.posix_openptletgrantpt=ok_exnUnix_pseudo_terminal.grantptletunlockpt=ok_exnUnix_pseudo_terminal.unlockptletptsname=ok_exnUnix_pseudo_terminal.ptsnameletrun_neovim_with_pty~time_source~f=letpty_master=posix_openpt[O_RDWR;O_NOCTTY]ingrantptpty_master;unlockptpty_master;Expect_test_helpers_async.with_temp_dir(funtmp_dir->letsocket=tmp_dir^/"socket"inmatchCore_unix.fork()with|`In_the_child->let(_:int)=Core_unix.Terminal_io.setsid()inletpty_slave=Core_unix.openfile~mode:[O_RDWR](ptsnamepty_master)inCore_unix.dup2~src:pty_slave~dst:Core_unix.stdin();Core_unix.dup2~src:pty_slave~dst:Core_unix.stdout();Core_unix.dup2~src:pty_slave~dst:Core_unix.stderr();Core_unix.closepty_slave;(* There is some undocumented internal limit for the socket length (it doesn't
appear in `:h limits`) so to ensure we create a socket we set the working dir
to [tmp_dir] and create the socket with a relative path. *)Core_unix.chdirtmp_dir;letprog=neovim_pathin(* We do *not* want to run with --headless here. *)Core_unix.exec()~prog~argv:[prog;"-n";"--clean";"--listen";"./socket"]~env:(`Extend["NVIM_RPLUGIN_MANIFEST","rplugin.vim"])|>never_returns|`In_the_parentnvim->with_process_cleanup~name:"nvim"nvim~f:(fun()->match%bindspin_until_nvim_creates_socket_filenvim~socketwith|`Nvim_crashedexit_or_signal->return(`Already_reapedexit_or_signal)|`Socket_missing->raise_s[%message"Socket was not created"]|`Socket_created->let%bindclient=socket_clientsocket?time_source>>|ok_exninletsend_keysbytes=letbuf=Bytes.of_stringbytesinletbytes_written=Core_unix.single_writepty_master~bufinassert(String.lengthbytes=bytes_written)inlet%bindresult=f~tmp_dir~client~send_keysinlet%map()=Client.closeclientin(matchresultwith|`Closed->`Need_to_reap`Patient|`Still_running->`Need_to_reap`Impatient)));;let%expect_test"Keyboard interrupt aborts simple RPC request"=Backtrace.elide:=true;let%bind()=run_neovim_with_pty~time_source:None~f:(fun~tmp_dir~client~send_keys->letfifo=tmp_dir^/"fifo"inlet%bind()=Unix.mkfifofifoinletsleep=run_join[%here]client([writefilefifo~contents:"Sleeping";Nvim.command"sleep 100"]|>Api_call.Or_error.all_unit)inlet%bindreader=Reader.open_filefifoinlet%bindmessage=Reader.read_linereaderinprint_s[%sexp(message:stringReader.Read_result.t)];send_keys"\003";let%bindsleep=sleepinprint_s[%message(sleep:unitOr_error.t)];let%bind()=attempt_to_quit~tmp_dir~clientinreturn`Closed)in[%expect{|
(Ok Sleeping)
(sleep
(Error
(("Called from"
lib/vcaml/test/semantics/test_keyboard_interrupts.ml:LINE:COL)
(("Vim returned error" "Keyboard interrupt" (error_type Exception))
(index 1)))))
("nvim exited" (exit_or_signal (Ok ()))) |}];Backtrace.elide:=false;return();;leton_keyboard_interrupt_abort_rpcrequest_and_notify_callback~timeout~time_source~f=run_neovim_with_pty~time_source~f:(fun~tmp_dir~client~send_keys->letfifo=tmp_dir^/"fifo"inlet%bind()=Unix.mkfifofifoinletsent_keys=Ivar.create()inletblock_nvim~client=letblocking=Ivar.create()inletfunction_name="rpc"inletcall_rpc=(wrap_viml_function~type_:Defun.Vim.(Integer@->String@->Nil@->returnNil)~function_name:"rpcrequest")(Client.channelclient)function_name()inregister_request_blockingclient~name:function_name~type_:Defun.Ocaml.Sync.(Nil@->returnNil)~f:(fun~keyboard_interrupted~client()->Ivar.fillblocking();uponkeyboard_interrupted(fun()->print_endline"Keyboard interrupt!");let%bind()=Ivar.readsent_keysinfclient);let%bindresult=run_join[%here]client([writefilefifo~contents:"Calling RPC";call_rpc]|>Api_call.Or_error.all_unit)inlet%map()=Ivar.readblockinginresultinletrpc_result=block_nvim~clientinlet%bindreader=Reader.open_filefifoinlet%bindmessage=Reader.read_linereaderinprint_s[%sexp(message:stringReader.Read_result.t)];send_keys"\003";Ivar.fillsent_keys();letrpc_result=let%bindrpc_result=rpc_resultinlet%bind()=attempt_to_quit~tmp_dir~clientinreturnrpc_resultinmatchtimeoutwith|None->let%maprpc_result=rpc_resultinprint_s[%message(rpc_result:unitOr_error.t)];`Closed|Sometimeout->let%maprpc_result=with_timeouttimeoutrpc_resultinprint_s[%message(rpc_result:unitOr_error.tClock_ns.Or_timeout.t)];(matchrpc_resultwith|`Timeout->`Still_running|`Result_->`Closed));;let%expect_test"Keyboard interrupt learned by RPC response aborts [rpcrequest] and \
notifies callback"=Backtrace.elide:=true;let%bind()=on_keyboard_interrupt_abort_rpcrequest_and_notify_callback~timeout:None~f:(funclient->let%mapresult=run_join[%here]client(Nvim.command"sleep 100")inprint_s[%message"Result after interrupt"~_:(result:unitOr_error.t)];result)~time_source:Nonein(* Observe that even though the request in the body of the RPC fails due to the keyboard
interrupt, the RPC returns an (Ok ()) result to ensure that control is returned
immediately to the user without displaying an error message. *)[%expect{|
(Ok "Calling RPC")
Keyboard interrupt!
("Result after interrupt"
(Error
(("Called from"
lib/vcaml/test/semantics/test_keyboard_interrupts.ml:LINE:COL)
("Vim returned error" "Keyboard interrupt" (error_type Exception)))))
(rpc_result (Ok ()))
("nvim exited" (exit_or_signal (Ok ()))) |}];Backtrace.elide:=false;return();;let%expect_test"Keyboard interrupt learned by heartbeating aborts [rpcrequest] and \
notifies callback"=let%bind()=on_keyboard_interrupt_abort_rpcrequest_and_notify_callback~timeout:None~time_source:(Some(Time_source.wall_clock()))~f:(fun_->Deferred.never()|>Deferred.ok)in[%expect{|
(Ok "Calling RPC")
Keyboard interrupt!
(rpc_result (Ok ()))
("nvim exited" (exit_or_signal (Ok ()))) |}];return();;let%expect_test"Keyboard interrupt learned by ??? - Neovim's semantics have changed!"=let%bind()=on_keyboard_interrupt_abort_rpcrequest_and_notify_callback~timeout:(SomeTime_float.Span.second)~time_source:None~f:(fun_->Deferred.never()|>Deferred.ok)in(* If this test succeeds then Neovim's semantics around when it alerts have changed. We
should investigate - heartbeating may no longer be required. *)[%expect{|
(Ok "Calling RPC")
(rpc_result Timeout)
("nvim exited" (exit_or_signal (Error (Exit_non_zero 1)))) |}];return();;[%%endif]