mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	 975c2124a6
			
		
	
	975c2124a6
	
	
	
		
			
			Problem: Tests that need to check `nvim` CLI behavior (no RPC session) create their own ad-hoc `system()` wrappers. Solution: - Use `n.spawn_wait` instead of `system()`. - Bonus: this also improves the tests by explicitly checking for `stdout` or `stderr`. And if a signal is raised, `ProcStream.status` will reflect it.
		
			
				
	
	
		
			264 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| ---
 | |
| --- Basic stream types.
 | |
| --- See `rpc_stream.lua` for the msgpack layer.
 | |
| ---
 | |
| 
 | |
| local uv = vim.uv
 | |
| 
 | |
| --- @class test.Stream
 | |
| --- @field write fun(self, data: string|string[])
 | |
| --- @field read_start fun(self, cb: fun(chunk: string))
 | |
| --- @field read_stop fun(self)
 | |
| --- @field close fun(self, signal?: string)
 | |
| 
 | |
| --- Stream over given pipes.
 | |
| ---
 | |
| --- @class vim.StdioStream : test.Stream
 | |
| --- @field private _in uv.uv_pipe_t
 | |
| --- @field private _out uv.uv_pipe_t
 | |
| local StdioStream = {}
 | |
| StdioStream.__index = StdioStream
 | |
| 
 | |
| function StdioStream.open()
 | |
|   local self = setmetatable({
 | |
|     _in = assert(uv.new_pipe(false)),
 | |
|     _out = assert(uv.new_pipe(false)),
 | |
|   }, StdioStream)
 | |
|   self._in:open(0)
 | |
|   self._out:open(1)
 | |
|   return self
 | |
| end
 | |
| 
 | |
| --- @param data string|string[]
 | |
| function StdioStream:write(data)
 | |
|   self._out:write(data)
 | |
| end
 | |
| 
 | |
| function StdioStream:read_start(cb)
 | |
|   self._in:read_start(function(err, chunk)
 | |
|     if err then
 | |
|       error(err)
 | |
|     end
 | |
|     cb(chunk)
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function StdioStream:read_stop()
 | |
|   self._in:read_stop()
 | |
| end
 | |
| 
 | |
| function StdioStream:close()
 | |
|   self._in:close()
 | |
|   self._out:close()
 | |
| end
 | |
| 
 | |
| --- Stream over a named pipe or TCP socket.
 | |
| ---
 | |
| --- @class test.SocketStream : test.Stream
 | |
| --- @field package _stream_error? string
 | |
| --- @field package _socket uv.uv_pipe_t
 | |
| local SocketStream = {}
 | |
| SocketStream.__index = SocketStream
 | |
| 
 | |
| function SocketStream.open(file)
 | |
|   local socket = assert(uv.new_pipe(false))
 | |
|   local self = setmetatable({
 | |
|     _socket = socket,
 | |
|     _stream_error = nil,
 | |
|   }, SocketStream)
 | |
|   uv.pipe_connect(socket, file, function(err)
 | |
|     self._stream_error = self._stream_error or err
 | |
|   end)
 | |
|   return self
 | |
| end
 | |
| 
 | |
| function SocketStream.connect(host, port)
 | |
|   local socket = assert(uv.new_tcp())
 | |
|   local self = setmetatable({
 | |
|     _socket = socket,
 | |
|     _stream_error = nil,
 | |
|   }, SocketStream)
 | |
|   uv.tcp_connect(socket, host, port, function(err)
 | |
|     self._stream_error = self._stream_error or err
 | |
|   end)
 | |
|   return self
 | |
| end
 | |
| 
 | |
| function SocketStream:write(data)
 | |
|   if self._stream_error then
 | |
|     error(self._stream_error)
 | |
|   end
 | |
|   uv.write(self._socket, data, function(err)
 | |
|     if err then
 | |
|       error(self._stream_error or err)
 | |
|     end
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function SocketStream:read_start(cb)
 | |
|   if self._stream_error then
 | |
|     error(self._stream_error)
 | |
|   end
 | |
|   uv.read_start(self._socket, function(err, chunk)
 | |
|     if err then
 | |
|       error(err)
 | |
|     end
 | |
|     cb(chunk)
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function SocketStream:read_stop()
 | |
|   if self._stream_error then
 | |
|     error(self._stream_error)
 | |
|   end
 | |
|   uv.read_stop(self._socket)
 | |
| end
 | |
| 
 | |
| function SocketStream:close()
 | |
|   uv.close(self._socket)
 | |
| end
 | |
| 
 | |
| --- Stream over child process stdio.
 | |
| ---
 | |
| --- @class test.ProcStream : test.Stream
 | |
| --- @field private _proc uv.uv_process_t
 | |
| --- @field private _pid integer
 | |
| --- @field private _child_stdin uv.uv_pipe_t
 | |
| --- @field private _child_stdout uv.uv_pipe_t
 | |
| --- @field private _child_stderr uv.uv_pipe_t
 | |
| --- Collects stdout (if `collect_text=true`). Treats data as text (CRLF converted to LF).
 | |
| --- @field stdout string
 | |
| --- Collects stderr as raw data.
 | |
| --- @field stderr string
 | |
| --- Gets stderr+stdout as text (CRLF converted to LF).
 | |
| --- @field output fun(): string
 | |
| --- @field stdout_eof boolean
 | |
| --- @field stderr_eof boolean
 | |
| --- Collects text into the `stdout` field.
 | |
| --- @field collect_text boolean
 | |
| --- Exit code
 | |
| --- @field status integer
 | |
| --- @field signal integer
 | |
| local ProcStream = {}
 | |
| ProcStream.__index = ProcStream
 | |
| 
 | |
| --- Starts child process specified by `argv`.
 | |
| ---
 | |
| --- @param argv string[]
 | |
| --- @param env string[]?
 | |
| --- @param io_extra uv.uv_pipe_t?
 | |
| --- @return test.ProcStream
 | |
| function ProcStream.spawn(argv, env, io_extra)
 | |
|   local self = setmetatable({
 | |
|     collect_text = false,
 | |
|     output = function(self)
 | |
|       if not self.collect_text then
 | |
|         error('set collect_text=true')
 | |
|       end
 | |
|       return (self.stderr .. self.stdout):gsub('\r\n', '\n')
 | |
|     end,
 | |
|     stdout = '',
 | |
|     stderr = '',
 | |
|     stdout_eof = false,
 | |
|     stderr_eof = false,
 | |
|     _child_stdin = assert(uv.new_pipe(false)),
 | |
|     _child_stdout = assert(uv.new_pipe(false)),
 | |
|     _child_stderr = assert(uv.new_pipe(false)),
 | |
|     _exiting = false,
 | |
|   }, ProcStream)
 | |
|   local prog = argv[1]
 | |
|   local args = {} --- @type string[]
 | |
|   for i = 2, #argv do
 | |
|     args[#args + 1] = argv[i]
 | |
|   end
 | |
|   --- @diagnostic disable-next-line:missing-fields
 | |
|   self._proc, self._pid = uv.spawn(prog, {
 | |
|     stdio = { self._child_stdin, self._child_stdout, self._child_stderr, io_extra },
 | |
|     args = args,
 | |
|     --- @diagnostic disable-next-line:assign-type-mismatch
 | |
|     env = env,
 | |
|   }, function(status, signal)
 | |
|     self.signal = signal
 | |
|     -- "Abort" exit may not set status; force to nonzero in that case.
 | |
|     self.status = (0 ~= (status or 0) or 0 == (signal or 0)) and status or (128 + (signal or 0))
 | |
|   end)
 | |
| 
 | |
|   if not self._proc then
 | |
|     local err = self._pid
 | |
|     error(err)
 | |
|   end
 | |
| 
 | |
|   return self
 | |
| end
 | |
| 
 | |
| function ProcStream:write(data)
 | |
|   self._child_stdin:write(data)
 | |
| end
 | |
| 
 | |
| function ProcStream:on_read(stream, cb, err, chunk)
 | |
|   if err then
 | |
|     error(err) -- stream read failed?
 | |
|   elseif chunk then
 | |
|     -- Always collect stderr, in case it gives useful info on failure.
 | |
|     if stream == 'stderr' then
 | |
|       self.stderr = self.stderr .. chunk --[[@as string]]
 | |
|     elseif stream == 'stdout' and self.collect_text then
 | |
|       -- Set `stdout` and convert CRLF => LF.
 | |
|       self.stdout = (self.stdout .. chunk):gsub('\r\n', '\n')
 | |
|     end
 | |
|   else
 | |
|     -- stderr_eof/stdout_eof
 | |
|     self[stream .. '_eof'] = true ---@type boolean
 | |
|   end
 | |
| 
 | |
|   -- Handler provided by the caller.
 | |
|   if cb then
 | |
|     cb(chunk)
 | |
|   end
 | |
| end
 | |
| 
 | |
| --- Collects output until the process exits.
 | |
| function ProcStream:wait()
 | |
|   while not (self.stdout_eof and self.stderr_eof and (self.status or self.signal)) do
 | |
|     uv.run('once')
 | |
|   end
 | |
| end
 | |
| 
 | |
| function ProcStream:read_start(on_stdout, on_stderr)
 | |
|   self._child_stdout:read_start(function(err, chunk)
 | |
|     self:on_read('stdout', on_stdout, err, chunk)
 | |
|   end)
 | |
|   self._child_stderr:read_start(function(err, chunk)
 | |
|     self:on_read('stderr', on_stderr, err, chunk)
 | |
|   end)
 | |
| end
 | |
| 
 | |
| function ProcStream:read_stop()
 | |
|   self._child_stdout:read_stop()
 | |
|   self._child_stderr:read_stop()
 | |
| end
 | |
| 
 | |
| function ProcStream:close(signal)
 | |
|   if self._closed then
 | |
|     return
 | |
|   end
 | |
|   self._closed = true
 | |
|   self:read_stop()
 | |
|   self._child_stdin:close()
 | |
|   self._child_stdout:close()
 | |
|   self._child_stderr:close()
 | |
|   if type(signal) == 'string' then
 | |
|     self._proc:kill('sig' .. signal)
 | |
|   end
 | |
|   while self.status == nil do
 | |
|     uv.run 'once'
 | |
|   end
 | |
|   return self.status, self.signal
 | |
| end
 | |
| 
 | |
| return {
 | |
|   StdioStream = StdioStream,
 | |
|   ProcStream = ProcStream,
 | |
|   SocketStream = SocketStream,
 | |
| }
 |