neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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)