server_spec.lua (11944B)
1 local t = require('test.testutil') 2 local n = require('test.functional.testnvim')() 3 4 local eq, neq, eval = t.eq, t.neq, n.eval 5 local clear, fn, api = n.clear, n.fn, n.api 6 local matches = t.matches 7 local pcall_err = t.pcall_err 8 local check_close = n.check_close 9 local mkdir = t.mkdir 10 local rmdir = n.rmdir 11 local is_os = t.is_os 12 13 local testlog = 'Xtest-server-log' 14 15 local function clear_serverlist() 16 for _, server in pairs(fn.serverlist()) do 17 fn.serverstop(server) 18 end 19 end 20 21 after_each(function() 22 check_close() 23 os.remove(testlog) 24 end) 25 26 before_each(function() 27 os.remove(testlog) 28 end) 29 30 describe('server', function() 31 it('serverstart() stores sockets in $XDG_RUNTIME_DIR', function() 32 local dir = 'Xtest_xdg_run' 33 mkdir(dir) 34 finally(function() 35 rmdir(dir) 36 end) 37 clear({ env = { XDG_RUNTIME_DIR = dir } }) 38 matches(dir, fn.stdpath('run')) 39 if not is_os('win') then 40 matches(dir, fn.serverstart()) 41 end 42 end) 43 44 it('broken $XDG_RUNTIME_DIR is not fatal #30282', function() 45 clear { 46 args_rm = { '--listen' }, 47 env = { NVIM_LOG_FILE = testlog, XDG_RUNTIME_DIR = '/non-existent-dir/subdir//' }, 48 } 49 50 if is_os('win') then 51 -- Windows pipes have a special namespace and thus aren't decided by $XDG_RUNTIME_DIR. 52 matches('nvim', api.nvim_get_vvar('servername')) 53 else 54 eq('', api.nvim_get_vvar('servername')) 55 t.assert_log('Failed to start server%: no such file or directory', testlog, 100) 56 end 57 end) 58 59 it('serverstart(), serverstop() does not set $NVIM', function() 60 clear() 61 local s = eval('serverstart()') 62 assert(s ~= nil and s:len() > 0, 'serverstart() returned empty') 63 eq('', eval('$NVIM')) 64 eq('', eval('$NVIM_LISTEN_ADDRESS')) 65 eq(1, eval("serverstop('" .. s .. "')")) 66 eq('', eval('$NVIM_LISTEN_ADDRESS')) 67 end) 68 69 it('sets v:servername at startup or if all servers were stopped', function() 70 clear() 71 local initial_server = api.nvim_get_vvar('servername') 72 assert(initial_server ~= nil and initial_server:len() > 0, 'v:servername was not initialized') 73 74 -- v:servername is readonly so we cannot unset it--but we can test that it 75 -- does not get set again thereafter. 76 local s = fn.serverstart() 77 assert(s ~= nil and s:len() > 0, 'serverstart() returned empty') 78 neq(initial_server, s) 79 80 -- serverstop() does _not_ modify v:servername... 81 eq(1, fn.serverstop(s)) 82 eq(initial_server, api.nvim_get_vvar('servername')) 83 84 -- ...unless we stop _all_ servers. 85 eq(1, fn.serverstop(fn.serverlist()[1])) 86 eq('', api.nvim_get_vvar('servername')) 87 88 -- v:servername and $NVIM take the next available server. 89 local servername = ( 90 is_os('win') and [[\\.\pipe\Xtest-functional-server-pipe]] 91 or './Xtest-functional-server-socket' 92 ) 93 fn.serverstart(servername) 94 eq(servername, api.nvim_get_vvar('servername')) 95 -- Not set in the current process, only in children. 96 eq('', eval('$NVIM')) 97 end) 98 99 it('serverstop() returns false for invalid input', function() 100 clear { 101 args_rm = { '--listen' }, 102 env = { 103 NVIM_LOG_FILE = testlog, 104 NVIM_LISTEN_ADDRESS = '', 105 }, 106 } 107 eq(0, eval("serverstop('')")) 108 eq(0, eval("serverstop('bogus-socket-name')")) 109 t.assert_log('Not listening on bogus%-socket%-name', testlog, 10) 110 end) 111 112 it('parses endpoints', function() 113 clear { 114 args_rm = { '--listen' }, 115 env = { 116 NVIM_LOG_FILE = testlog, 117 NVIM_LISTEN_ADDRESS = '', 118 }, 119 } 120 clear_serverlist() 121 eq({}, fn.serverlist()) 122 123 local s = fn.serverstart('127.0.0.1:0') -- assign random port 124 if #s > 0 then 125 matches('127.0.0.1:%d+', s) 126 eq(s, fn.serverlist()[1]) 127 clear_serverlist() 128 end 129 130 s = fn.serverstart('127.0.0.1:') -- assign random port 131 if #s > 0 then 132 matches('127.0.0.1:%d+', s) 133 eq(s, fn.serverlist()[1]) 134 clear_serverlist() 135 end 136 137 local expected = {} 138 local v4 = '127.0.0.1:12345' 139 local status, _ = pcall(fn.serverstart, v4) 140 if status then 141 table.insert(expected, v4) 142 pcall(fn.serverstart, v4) -- exists already; ignore 143 t.assert_log('Failed to start server: address already in use: 127%.0%.0%.1', testlog, 10) 144 end 145 146 local v6 = '::1:12345' 147 status, _ = pcall(fn.serverstart, v6) 148 if status then 149 table.insert(expected, v6) 150 pcall(fn.serverstart, v6) -- exists already; ignore 151 t.assert_log('Failed to start server: address already in use: ::1', testlog, 10) 152 end 153 eq(expected, fn.serverlist()) 154 clear_serverlist() 155 156 -- Address without slashes is a "name" which is appended to a generated path. #8519 157 matches([[[/\\]xtest1%.2%.3%.4[^/\\]*]], fn.serverstart('xtest1.2.3.4')) 158 clear_serverlist() 159 160 eq('Vim:Failed to start server: invalid argument', pcall_err(fn.serverstart, '127.0.0.1:65536')) -- invalid port 161 eq({}, fn.serverlist()) 162 end) 163 164 it('serverlist() returns the list of servers', function() 165 -- Set XDG_RUNTIME_DIR to a temp dir in this session to properly test serverlist({peer = true}). See #35492 166 local tmp_dir = assert(vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/XXXXXX')) 167 local current_server = clear({ env = { XDG_RUNTIME_DIR = tmp_dir } }) 168 -- There should already be at least one server. 169 local _n = eval('len(serverlist())') 170 171 -- Add some servers. 172 local servs = ( 173 is_os('win') and { [[\\.\pipe\Xtest-pipe0934]], [[\\.\pipe\Xtest-pipe4324]] } 174 or { [[./Xtest-pipe0934]], [[./Xtest-pipe4324]] } 175 ) 176 for _, s in ipairs(servs) do 177 eq(s, eval("serverstart('" .. s .. "')")) 178 end 179 180 local new_servs = eval('serverlist()') 181 182 -- Exactly #servs servers should be added. 183 eq(_n + #servs, #new_servs) 184 -- The new servers should be at the end of the list. 185 for i = 1, #servs do 186 eq(servs[i], new_servs[i + _n]) 187 eq(1, eval("serverstop('" .. servs[i] .. "')")) 188 end 189 -- After serverstop() the servers should NOT be in the list. 190 eq(_n, eval('len(serverlist())')) 191 192 -- serverlist({peer=true}) returns servers from other Nvim sessions. 193 if t.is_os('win') then 194 return 195 end 196 197 local old_servs_num = #fn.serverlist({ peer = true }) 198 local peer_temp = n.new_pipename() 199 local peer_name = peer_temp:match('[^/]*$') 200 201 local tmp_dir2 = assert(vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/XXXXXX')) 202 local peer_addr = ('%s/%s'):format(tmp_dir2, peer_name) 203 -- Set XDG_RUNTIME_DIR to a temp dir in this session to properly test serverlist({peer = true}). See #35492 204 local client = n.new_session(true, { 205 args = { '--clean', '--listen', peer_addr, '--embed' }, 206 env = { XDG_RUNTIME_DIR = tmp_dir2 }, 207 merge = false, 208 }) 209 n.set_session(client) 210 eq(peer_addr, fn.serverlist()[1]) 211 212 n.set_session(current_server) 213 214 new_servs = fn.serverlist({ peer = true }) 215 local servers_without_peer = fn.serverlist() 216 eq(true, vim.list_contains(new_servs, peer_addr)) 217 eq(true, #servers_without_peer < #new_servs) 218 eq(true, old_servs_num < #new_servs) 219 client:close() 220 end) 221 222 it('removes stale socket files automatically #26053', function() 223 -- Windows named pipes are ephemeral kernel objects that are automatically 224 -- cleaned up when the process terminates. Unix domain sockets persist as 225 -- files on the filesystem and can become stale after crashes. 226 t.skip(is_os('win'), 'N/A on Windows') 227 228 clear() 229 clear_serverlist() 230 local socket_path = './Xtest-stale-socket' 231 232 -- Create stale socket file (simulate crash) 233 vim.uv.fs_close(vim.uv.fs_open(socket_path, 'w', 438)) 234 235 -- serverstart() should detect and remove stale socket 236 eq(socket_path, fn.serverstart(socket_path)) 237 fn.serverstop(socket_path) 238 239 -- Same test with --listen flag 240 vim.uv.fs_close(vim.uv.fs_open(socket_path, 'w', 438)) 241 clear({ args = { '--listen', socket_path } }) 242 eq(socket_path, api.nvim_get_vvar('servername')) 243 fn.serverstop(socket_path) 244 end) 245 246 it('does not remove live sockets #26053', function() 247 t.skip(is_os('win'), 'N/A on Windows') 248 249 clear() 250 local socket_path = './Xtest-live-socket' 251 eq(socket_path, fn.serverstart(socket_path)) 252 253 -- Second instance should fail without removing live socket 254 local result = n.exec_lua(function(sock) 255 return vim 256 .system( 257 { vim.v.progpath, '--headless', '--listen', sock }, 258 { text = true, env = { NVIM_LOG_FILE = testlog } } 259 ) 260 :wait() 261 end, socket_path) 262 t.assert_log('Socket already in use by another Nvim instance: ', testlog, 100) 263 t.assert_log('Failed to start server: address already in use: ', testlog, 100) 264 265 neq(0, result.code) 266 matches('Failed.*listen', result.stderr) 267 fn.serverstop(socket_path) 268 end) 269 end) 270 271 describe('startup --listen', function() 272 -- Tests Nvim output when failing to start, with and without "--headless". 273 local function _test(args, env, expected) 274 local function run(cmd) 275 return n.spawn_wait { 276 merge = false, 277 args = cmd, 278 env = vim.tbl_extend( 279 'force', 280 -- Avoid noise in the logs; we expect failures for these tests. 281 { NVIM_LOG_FILE = testlog }, 282 env or {} 283 ), 284 } 285 end 286 287 local cmd = vim.list_extend({ '--clean', '+qall!', '--headless' }, args) 288 local r = run(cmd) 289 eq(1, r.status) 290 matches(expected, r:output():gsub('\\n', ' ')) 291 292 if is_os('win') then 293 return -- On Windows, output without --headless is garbage. 294 end 295 table.remove(cmd, 3) -- Remove '--headless'. 296 assert(not vim.tbl_contains(cmd, '--headless')) 297 r = run(cmd) 298 eq(1, r.status) 299 matches(expected, r:output():gsub('\\n', ' ')) 300 end 301 302 it('validates', function() 303 clear { env = { NVIM_LOG_FILE = testlog } } 304 local in_use = n.eval('v:servername') ---@type string Address already used by another server. 305 306 t.assert_nolog('Failed to start server', testlog, 100) 307 t.assert_nolog('Host lookup failed', testlog, 100) 308 309 _test({ '--listen' }, nil, 'nvim.*: Argument missing after: "%-%-listen"') 310 _test({ '--listen2' }, nil, 'nvim.*: Garbage after option argument: "%-%-listen2"') 311 _test( 312 { '--listen', in_use }, 313 nil, 314 ('nvim.*: Failed to %%-%%-listen: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use)) 315 ) 316 _test({ '--listen', '/' }, nil, 'nvim.*: Failed to %-%-listen: [^:]+: "/"') 317 _test( 318 { '--listen', 'https://example.com' }, 319 nil, 320 ('nvim.*: Failed to %%-%%-listen: %s: "https://example.com"'):format( 321 is_os('mac') and 'unknown node or service' or 'service not available for socket type' 322 ) 323 ) 324 325 t.assert_log('Failed to start server', testlog, 100) 326 t.assert_log('Host lookup failed', testlog, 100) 327 328 _test( 329 {}, 330 { NVIM_LISTEN_ADDRESS = in_use }, 331 ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use)) 332 ) 333 _test({}, { NVIM_LISTEN_ADDRESS = '/' }, 'nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+: "/"') 334 _test( 335 {}, 336 { NVIM_LISTEN_ADDRESS = 'https://example.com' }, 337 ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: %s: "https://example.com"'):format( 338 is_os('mac') and 'unknown node or service' or 'service not available for socket type' 339 ) 340 ) 341 end) 342 343 it('sets v:servername, overrides $NVIM_LISTEN_ADDRESS', function() 344 local addr = (is_os('win') and [[\\.\pipe\Xtest-listen-pipe]] or './Xtest-listen-pipe') 345 clear({ env = { NVIM_LISTEN_ADDRESS = './Xtest-env-pipe' }, args = { '--listen', addr } }) 346 eq('', eval('$NVIM_LISTEN_ADDRESS')) -- Cleared on startup. 347 eq(addr, api.nvim_get_vvar('servername')) 348 349 -- Address without slashes is a "name" which is appended to a generated path. #8519 350 clear({ args = { '--listen', 'test-name' } }) 351 matches([[[/\\]test%-name[^/\\]*]], api.nvim_get_vvar('servername')) 352 end) 353 end)