commit 8a2aec99748229ad9d1e12c1cbc0768d063e8eed
parent 3a881132460430d23f2fdc87822c87d47f721cfc
Author: Justin M. Keyes <justinkz@gmail.com>
Date: Sun, 8 Sep 2024 12:48:32 -0700
fix(startup): server fails if $NVIM_APPNAME is relative dir #30310
Problem:
If $NVIM_APPNAME is a relative dir path, Nvim fails to start its
primary/default server, and `v:servername` is empty.
Root cause is d34c64e342dfba9248d1055e702d02620a1b31a8, but this wasn't
noticed until 96128a5076b7 started reporting the error more loudly.
Solution:
- `server_address_new`: replace slashes "/" in the appname before using
it as a servername.
- `vim_mktempdir`: always prefer the system-wide top-level "nvim.user/"
directory. That isn't intended to be specific to NVIM_APPNAME; rather,
each *subdirectory* ("nvim.user/xxx") is owned by each Nvim instance.
Nvim "apps" can be identified by the server socket(s) stored in those
per-Nvim subdirs.
fix #30256
Diffstat:
11 files changed, 328 insertions(+), 315 deletions(-)
diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c
@@ -7641,7 +7641,7 @@ static void get_xdg_var_list(const XDGVarType xdg, typval_T *rettv)
return;
}
const void *iter = NULL;
- const char *appname = get_appname();
+ const char *appname = get_appname(false);
do {
size_t dir_len;
const char *dir;
diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c
@@ -3264,18 +3264,12 @@ static void vim_mktempdir(void)
char tmp[TEMP_FILE_PATH_MAXLEN];
char path[TEMP_FILE_PATH_MAXLEN];
char user[40] = { 0 };
- char appname[40] = { 0 };
os_get_username(user, sizeof(user));
// Usernames may contain slashes! #19240
memchrsub(user, '/', '_', sizeof(user));
memchrsub(user, '\\', '_', sizeof(user));
- // Appname may be a relative path, replace slashes to make it name-like.
- xstrlcpy(appname, get_appname(), sizeof(appname));
- memchrsub(appname, '/', '%', sizeof(appname));
- memchrsub(appname, '\\', '%', sizeof(appname));
-
// Make sure the umask doesn't remove the executable bit.
// "repl" has been reported to use "0177".
mode_t umask_save = umask(0077);
@@ -3283,14 +3277,15 @@ static void vim_mktempdir(void)
// Expand environment variables, leave room for "/tmp/nvim.<user>/XXXXXX/999999999".
expand_env((char *)temp_dirs[i], tmp, TEMP_FILE_PATH_MAXLEN - 64);
if (!os_isdir(tmp)) {
+ if (strequal("$TMPDIR", temp_dirs[i])) {
+ WLOG("$TMPDIR tempdir not a directory (or does not exist): %s", tmp);
+ }
continue;
}
// "/tmp/" exists, now try to create "/tmp/nvim.<user>/".
add_pathsep(tmp);
-
- xstrlcat(tmp, appname, sizeof(tmp));
- xstrlcat(tmp, ".", sizeof(tmp));
+ xstrlcat(tmp, "nvim.", sizeof(tmp));
xstrlcat(tmp, user, sizeof(tmp));
os_mkdir(tmp, 0700); // Always create, to avoid a race.
bool owned = os_file_owned(tmp);
diff --git a/src/nvim/msgpack_rpc/server.c b/src/nvim/msgpack_rpc/server.c
@@ -121,14 +121,15 @@ char *server_address_new(const char *name)
{
static uint32_t count = 0;
char fmt[ADDRESS_MAX_SIZE];
- const char *appname = get_appname();
#ifdef MSWIN
+ (void)get_appname(true);
int r = snprintf(fmt, sizeof(fmt), "\\\\.\\pipe\\%s.%" PRIu64 ".%" PRIu32,
- name ? name : appname, os_get_pid(), count++);
+ name ? name : NameBuff, os_get_pid(), count++);
#else
char *dir = stdpaths_get_xdg_var(kXDGRuntimeDir);
+ (void)get_appname(true);
int r = snprintf(fmt, sizeof(fmt), "%s/%s.%" PRIu64 ".%" PRIu32,
- dir, name ? name : appname, os_get_pid(), count++);
+ dir, name ? name : NameBuff, os_get_pid(), count++);
xfree(dir);
#endif
if ((size_t)r >= sizeof(fmt)) {
diff --git a/src/nvim/os/stdpaths.c b/src/nvim/os/stdpaths.c
@@ -63,22 +63,32 @@ static const char *const xdg_defaults[] = {
#endif
};
-/// Get the value of $NVIM_APPNAME or "nvim" if not set.
+/// Gets the value of $NVIM_APPNAME, or "nvim" if not set.
+///
+/// @param namelike Write "name-like" value (no path separators) in `NameBuff`.
///
/// @return $NVIM_APPNAME value
-const char *get_appname(void)
+const char *get_appname(bool namelike)
{
const char *env_val = os_getenv("NVIM_APPNAME");
if (env_val == NULL || *env_val == NUL) {
env_val = "nvim";
}
+
+ if (namelike) {
+ // Appname may be a relative path, replace slashes to make it name-like.
+ xstrlcpy(NameBuff, env_val, sizeof(NameBuff));
+ memchrsub(NameBuff, '/', '-', sizeof(NameBuff));
+ memchrsub(NameBuff, '\\', '-', sizeof(NameBuff));
+ }
+
return env_val;
}
/// Ensure that APPNAME is valid. Must be a name or relative path.
bool appname_is_valid(void)
{
- const char *appname = get_appname();
+ const char *appname = get_appname(false);
if (path_is_absolute(appname)
// TODO(justinmk): on Windows, path_is_absolute says "/" is NOT absolute. Should it?
|| strequal(appname, "/")
@@ -193,7 +203,7 @@ char *get_xdg_home(const XDGVarType idx)
FUNC_ATTR_WARN_UNUSED_RESULT
{
char *dir = stdpaths_get_xdg_var(idx);
- const char *appname = get_appname();
+ const char *appname = get_appname(false);
size_t appname_len = strlen(appname);
assert(appname_len < (IOSIZE - sizeof("-data")));
diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c
@@ -1559,7 +1559,7 @@ static inline char *add_env_sep_dirs(char *dest, const char *const val, const ch
return dest;
}
const void *iter = NULL;
- const char *appname = get_appname();
+ const char *appname = get_appname(false);
const size_t appname_len = strlen(appname);
do {
size_t dir_len;
@@ -1626,7 +1626,7 @@ static inline char *add_dir(char *dest, const char *const dir, const size_t dir_
if (!after_pathsep(dest - 1, dest)) {
*dest++ = PATHSEP;
}
- const char *appname = get_appname();
+ const char *appname = get_appname(false);
size_t appname_len = strlen(appname);
assert(appname_len < (IOSIZE - sizeof("-data")));
xmemcpyz(IObuff, appname, appname_len);
@@ -1697,7 +1697,7 @@ char *runtimepath_default(bool clean_arg)
size_t config_len = 0;
size_t vimruntime_len = 0;
size_t libdir_len = 0;
- const char *appname = get_appname();
+ const char *appname = get_appname(false);
size_t appname_len = strlen(appname);
if (data_home != NULL) {
data_len = strlen(data_home);
diff --git a/test/functional/core/fileio_spec.lua b/test/functional/core/fileio_spec.lua
@@ -321,11 +321,11 @@ end)
describe('tmpdir', function()
local tmproot_pat = [=[.*[/\\]nvim%.[^/\\]+]=]
local testlog = 'Xtest_tmpdir_log'
- local os_tmpdir
+ local os_tmpdir ---@type string
before_each(function()
-- Fake /tmp dir so that we can mess it up.
- os_tmpdir = vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX')
+ os_tmpdir = assert(vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX'))
end)
after_each(function()
@@ -414,15 +414,4 @@ describe('tmpdir', function()
rm_tmpdir()
eq('E5431: tempdir disappeared (3 times)', api.nvim_get_vvar('errmsg'))
end)
-
- it('$NVIM_APPNAME relative path', function()
- clear({
- env = {
- NVIM_APPNAME = 'a/b',
- NVIM_LOG_FILE = testlog,
- TMPDIR = os_tmpdir,
- },
- })
- matches([=[.*[/\\]a%%b%.[^/\\]+]=], fn.tempname())
- end)
end)
diff --git a/test/functional/core/server_spec.lua b/test/functional/core/server_spec.lua
@@ -0,0 +1,278 @@
+local t = require('test.testutil')
+local n = require('test.functional.testnvim')()
+
+local eq, neq, eval = t.eq, t.neq, n.eval
+local clear, fn, api = n.clear, n.fn, n.api
+local matches = t.matches
+local pcall_err = t.pcall_err
+local check_close = n.check_close
+local mkdir = t.mkdir
+local rmdir = n.rmdir
+local is_os = t.is_os
+
+local testlog = 'Xtest-server-log'
+
+local function clear_serverlist()
+ for _, server in pairs(fn.serverlist()) do
+ fn.serverstop(server)
+ end
+end
+
+after_each(function()
+ check_close()
+ os.remove(testlog)
+end)
+
+before_each(function()
+ os.remove(testlog)
+end)
+
+describe('server', function()
+ it('serverstart() stores sockets in $XDG_RUNTIME_DIR', function()
+ local dir = 'Xtest_xdg_run'
+ mkdir(dir)
+ finally(function()
+ rmdir(dir)
+ end)
+ clear({ env = { XDG_RUNTIME_DIR = dir } })
+ matches(dir, fn.stdpath('run'))
+ if not is_os('win') then
+ matches(dir, fn.serverstart())
+ end
+ end)
+
+ it('broken $XDG_RUNTIME_DIR is not fatal #30282', function()
+ clear {
+ args_rm = { '--listen' },
+ env = { NVIM_LOG_FILE = testlog, XDG_RUNTIME_DIR = '/non-existent-dir/subdir//' },
+ }
+
+ if is_os('win') then
+ -- Windows pipes have a special namespace and thus aren't decided by $XDG_RUNTIME_DIR.
+ matches('nvim', api.nvim_get_vvar('servername'))
+ else
+ eq('', api.nvim_get_vvar('servername'))
+ t.assert_log('Failed to start server%: no such file or directory', testlog, 100)
+ end
+ end)
+
+ it('serverstart(), serverstop() does not set $NVIM', function()
+ clear()
+ local s = eval('serverstart()')
+ assert(s ~= nil and s:len() > 0, 'serverstart() returned empty')
+ eq('', eval('$NVIM'))
+ eq('', eval('$NVIM_LISTEN_ADDRESS'))
+ eq(1, eval("serverstop('" .. s .. "')"))
+ eq('', eval('$NVIM_LISTEN_ADDRESS'))
+ end)
+
+ it('sets v:servername at startup or if all servers were stopped', function()
+ clear()
+ local initial_server = api.nvim_get_vvar('servername')
+ assert(initial_server ~= nil and initial_server:len() > 0, 'v:servername was not initialized')
+
+ -- v:servername is readonly so we cannot unset it--but we can test that it
+ -- does not get set again thereafter.
+ local s = fn.serverstart()
+ assert(s ~= nil and s:len() > 0, 'serverstart() returned empty')
+ neq(initial_server, s)
+
+ -- serverstop() does _not_ modify v:servername...
+ eq(1, fn.serverstop(s))
+ eq(initial_server, api.nvim_get_vvar('servername'))
+
+ -- ...unless we stop _all_ servers.
+ eq(1, fn.serverstop(fn.serverlist()[1]))
+ eq('', api.nvim_get_vvar('servername'))
+
+ -- v:servername and $NVIM take the next available server.
+ local servername = (
+ is_os('win') and [[\\.\pipe\Xtest-functional-server-pipe]]
+ or './Xtest-functional-server-socket'
+ )
+ fn.serverstart(servername)
+ eq(servername, api.nvim_get_vvar('servername'))
+ -- Not set in the current process, only in children.
+ eq('', eval('$NVIM'))
+ end)
+
+ it('serverstop() returns false for invalid input', function()
+ clear {
+ args_rm = { '--listen' },
+ env = {
+ NVIM_LOG_FILE = testlog,
+ NVIM_LISTEN_ADDRESS = '',
+ },
+ }
+ eq(0, eval("serverstop('')"))
+ eq(0, eval("serverstop('bogus-socket-name')"))
+ t.assert_log('Not listening on bogus%-socket%-name', testlog, 10)
+ end)
+
+ it('parses endpoints', function()
+ clear {
+ args_rm = { '--listen' },
+ env = {
+ NVIM_LOG_FILE = testlog,
+ NVIM_LISTEN_ADDRESS = '',
+ },
+ }
+ clear_serverlist()
+ eq({}, fn.serverlist())
+
+ local s = fn.serverstart('127.0.0.1:0') -- assign random port
+ if #s > 0 then
+ matches('127.0.0.1:%d+', s)
+ eq(s, fn.serverlist()[1])
+ clear_serverlist()
+ end
+
+ s = fn.serverstart('127.0.0.1:') -- assign random port
+ if #s > 0 then
+ matches('127.0.0.1:%d+', s)
+ eq(s, fn.serverlist()[1])
+ clear_serverlist()
+ end
+
+ local expected = {}
+ local v4 = '127.0.0.1:12345'
+ local status, _ = pcall(fn.serverstart, v4)
+ if status then
+ table.insert(expected, v4)
+ pcall(fn.serverstart, v4) -- exists already; ignore
+ t.assert_log('Failed to start server: address already in use: 127%.0%.0%.1', testlog, 10)
+ end
+
+ local v6 = '::1:12345'
+ status, _ = pcall(fn.serverstart, v6)
+ if status then
+ table.insert(expected, v6)
+ pcall(fn.serverstart, v6) -- exists already; ignore
+ t.assert_log('Failed to start server: address already in use: ::1', testlog, 10)
+ end
+ eq(expected, fn.serverlist())
+ clear_serverlist()
+
+ -- Address without slashes is a "name" which is appended to a generated path. #8519
+ matches([[[/\\]xtest1%.2%.3%.4[^/\\]*]], fn.serverstart('xtest1.2.3.4'))
+ clear_serverlist()
+
+ eq('Vim:Failed to start server: invalid argument', pcall_err(fn.serverstart, '127.0.0.1:65536')) -- invalid port
+ eq({}, fn.serverlist())
+ end)
+
+ it('serverlist() returns the list of servers', function()
+ clear()
+ -- There should already be at least one server.
+ local _n = eval('len(serverlist())')
+
+ -- Add some servers.
+ local servs = (
+ is_os('win') and { [[\\.\pipe\Xtest-pipe0934]], [[\\.\pipe\Xtest-pipe4324]] }
+ or { [[./Xtest-pipe0934]], [[./Xtest-pipe4324]] }
+ )
+ for _, s in ipairs(servs) do
+ eq(s, eval("serverstart('" .. s .. "')"))
+ end
+
+ local new_servs = eval('serverlist()')
+
+ -- Exactly #servs servers should be added.
+ eq(_n + #servs, #new_servs)
+ -- The new servers should be at the end of the list.
+ for i = 1, #servs do
+ eq(servs[i], new_servs[i + _n])
+ eq(1, eval("serverstop('" .. servs[i] .. "')"))
+ end
+ -- After serverstop() the servers should NOT be in the list.
+ eq(_n, eval('len(serverlist())'))
+ end)
+end)
+
+describe('startup --listen', function()
+ -- Tests Nvim output when failing to start, with and without "--headless".
+ -- TODO(justinmk): clear() should have a way to get stdout if Nvim fails to start.
+ local function _test(args, env, expected)
+ local function run(cmd)
+ return n.exec_lua(function(cmd_, env_)
+ return vim
+ .system(cmd_, {
+ text = true,
+ env = vim.tbl_extend(
+ 'force',
+ -- Avoid noise in the logs; we expect failures for these tests.
+ { NVIM_LOG_FILE = testlog },
+ env_ or {}
+ ),
+ })
+ :wait()
+ end, cmd, env) --[[@as vim.SystemCompleted]]
+ end
+
+ local cmd = vim.list_extend({ n.nvim_prog, '+qall!', '--headless' }, args)
+ local r = run(cmd)
+ eq(1, r.code)
+ matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' '))
+
+ if is_os('win') then
+ return -- On Windows, output without --headless is garbage.
+ end
+ table.remove(cmd, 3) -- Remove '--headless'.
+ assert(not vim.tbl_contains(cmd, '--headless'))
+ r = run(cmd)
+ eq(1, r.code)
+ matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' '))
+ end
+
+ it('validates', function()
+ clear { env = { NVIM_LOG_FILE = testlog } }
+ local in_use = n.eval('v:servername') ---@type string Address already used by another server.
+
+ t.assert_nolog('Failed to start server', testlog, 100)
+ t.assert_nolog('Host lookup failed', testlog, 100)
+
+ _test({ '--listen' }, nil, 'nvim.*: Argument missing after: "%-%-listen"')
+ _test({ '--listen2' }, nil, 'nvim.*: Garbage after option argument: "%-%-listen2"')
+ _test(
+ { '--listen', in_use },
+ nil,
+ ('nvim.*: Failed to %%-%%-listen: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use))
+ )
+ _test({ '--listen', '/' }, nil, 'nvim.*: Failed to %-%-listen: [^:]+: "/"')
+ _test(
+ { '--listen', 'https://example.com' },
+ nil,
+ ('nvim.*: Failed to %%-%%-listen: %s: "https://example.com"'):format(
+ is_os('mac') and 'unknown node or service' or 'service not available for socket type'
+ )
+ )
+
+ t.assert_log('Failed to start server', testlog, 100)
+ t.assert_log('Host lookup failed', testlog, 100)
+
+ _test(
+ {},
+ { NVIM_LISTEN_ADDRESS = in_use },
+ ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use))
+ )
+ _test({}, { NVIM_LISTEN_ADDRESS = '/' }, 'nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+: "/"')
+ _test(
+ {},
+ { NVIM_LISTEN_ADDRESS = 'https://example.com' },
+ ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: %s: "https://example.com"'):format(
+ is_os('mac') and 'unknown node or service' or 'service not available for socket type'
+ )
+ )
+ end)
+
+ it('sets v:servername, overrides $NVIM_LISTEN_ADDRESS', function()
+ local addr = (is_os('win') and [[\\.\pipe\Xtest-listen-pipe]] or './Xtest-listen-pipe')
+ clear({ env = { NVIM_LISTEN_ADDRESS = './Xtest-env-pipe' }, args = { '--listen', addr } })
+ eq('', eval('$NVIM_LISTEN_ADDRESS')) -- Cleared on startup.
+ eq(addr, api.nvim_get_vvar('servername'))
+
+ -- Address without slashes is a "name" which is appended to a generated path. #8519
+ clear({ args = { '--listen', 'test-name' } })
+ matches([[[/\\]test%-name[^/\\]*]], api.nvim_get_vvar('servername'))
+ end)
+end)
diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua
@@ -915,7 +915,7 @@ describe('stdpath()', function()
assert_alive() -- Check for crash. #8393
end)
- it('supports $NVIM_APPNAME', function()
+ it('$NVIM_APPNAME', function()
local appname = 'NVIM_APPNAME_TEST' .. ('_'):rep(106)
clear({ env = { NVIM_APPNAME = appname, NVIM_LOG_FILE = testlog } })
eq(appname, fn.fnamemodify(fn.stdpath('config'), ':t'))
@@ -957,6 +957,25 @@ describe('stdpath()', function()
test_appname('a/b\\c', 0)
end)
+ it('$NVIM_APPNAME relative path', function()
+ local tmpdir = t.tmpname(false)
+ t.mkdir(tmpdir)
+
+ clear({
+ args_rm = { '--listen' },
+ env = {
+ NVIM_APPNAME = 'relative/appname',
+ NVIM_LOG_FILE = testlog,
+ TMPDIR = tmpdir,
+ },
+ })
+
+ t.matches(vim.pesc(tmpdir), fn.tempname():gsub('\\', '/'))
+ t.assert_nolog('tempdir', testlog, 100)
+ t.assert_nolog('TMPDIR', testlog, 100)
+ t.matches([=[[/\\]relative%-appname.[^/\\]+]=], api.nvim_get_vvar('servername'))
+ end)
+
describe('returns a String', function()
describe('with "config"', function()
it('knows XDG_CONFIG_HOME', function()
diff --git a/test/functional/testnvim.lua b/test/functional/testnvim.lua
@@ -14,8 +14,7 @@ local is_os = t.is_os
local ok = t.ok
local sleep = uv.sleep
---- This module uses functions from the context of the test session, i.e. in the context of the
---- nvim being tests.
+--- Functions executing in the current nvim session/process being tested.
local M = {}
local runtime_set = 'set runtimepath^=./build/lib/nvim/'
diff --git a/test/functional/vimscript/server_spec.lua b/test/functional/vimscript/server_spec.lua
@@ -1,278 +0,0 @@
-local t = require('test.testutil')
-local n = require('test.functional.testnvim')()
-
-local eq, neq, eval = t.eq, t.neq, n.eval
-local clear, fn, api = n.clear, n.fn, n.api
-local matches = t.matches
-local pcall_err = t.pcall_err
-local check_close = n.check_close
-local mkdir = t.mkdir
-local rmdir = n.rmdir
-local is_os = t.is_os
-
-local testlog = 'Xtest-server-log'
-
-local function clear_serverlist()
- for _, server in pairs(fn.serverlist()) do
- fn.serverstop(server)
- end
-end
-
-after_each(function()
- check_close()
- os.remove(testlog)
-end)
-
-before_each(function()
- os.remove(testlog)
-end)
-
-describe('server', function()
- it('serverstart() stores sockets in $XDG_RUNTIME_DIR', function()
- local dir = 'Xtest_xdg_run'
- mkdir(dir)
- finally(function()
- rmdir(dir)
- end)
- clear({ env = { XDG_RUNTIME_DIR = dir } })
- matches(dir, fn.stdpath('run'))
- if not is_os('win') then
- matches(dir, fn.serverstart())
- end
- end)
-
- it('broken $XDG_RUNTIME_DIR is not fatal #30282', function()
- clear {
- args_rm = { '--listen' },
- env = { NVIM_LOG_FILE = testlog, XDG_RUNTIME_DIR = '/non-existent-dir/subdir//' },
- }
-
- if is_os('win') then
- -- Windows pipes have a special namespace and thus aren't decided by $XDG_RUNTIME_DIR.
- matches('nvim', api.nvim_get_vvar('servername'))
- else
- eq('', api.nvim_get_vvar('servername'))
- t.assert_log('Failed to start server%: no such file or directory', testlog, 100)
- end
- end)
-
- it('serverstart(), serverstop() does not set $NVIM', function()
- clear()
- local s = eval('serverstart()')
- assert(s ~= nil and s:len() > 0, 'serverstart() returned empty')
- eq('', eval('$NVIM'))
- eq('', eval('$NVIM_LISTEN_ADDRESS'))
- eq(1, eval("serverstop('" .. s .. "')"))
- eq('', eval('$NVIM_LISTEN_ADDRESS'))
- end)
-
- it('sets v:servername at startup or if all servers were stopped', function()
- clear()
- local initial_server = api.nvim_get_vvar('servername')
- assert(initial_server ~= nil and initial_server:len() > 0, 'v:servername was not initialized')
-
- -- v:servername is readonly so we cannot unset it--but we can test that it
- -- does not get set again thereafter.
- local s = fn.serverstart()
- assert(s ~= nil and s:len() > 0, 'serverstart() returned empty')
- neq(initial_server, s)
-
- -- serverstop() does _not_ modify v:servername...
- eq(1, fn.serverstop(s))
- eq(initial_server, api.nvim_get_vvar('servername'))
-
- -- ...unless we stop _all_ servers.
- eq(1, fn.serverstop(fn.serverlist()[1]))
- eq('', api.nvim_get_vvar('servername'))
-
- -- v:servername and $NVIM take the next available server.
- local servername = (
- is_os('win') and [[\\.\pipe\Xtest-functional-server-pipe]]
- or './Xtest-functional-server-socket'
- )
- fn.serverstart(servername)
- eq(servername, api.nvim_get_vvar('servername'))
- -- Not set in the current process, only in children.
- eq('', eval('$NVIM'))
- end)
-
- it('serverstop() returns false for invalid input', function()
- clear {
- args_rm = { '--listen' },
- env = {
- NVIM_LOG_FILE = testlog,
- NVIM_LISTEN_ADDRESS = '',
- },
- }
- eq(0, eval("serverstop('')"))
- eq(0, eval("serverstop('bogus-socket-name')"))
- t.assert_log('Not listening on bogus%-socket%-name', testlog, 10)
- end)
-
- it('parses endpoints', function()
- clear {
- args_rm = { '--listen' },
- env = {
- NVIM_LOG_FILE = testlog,
- NVIM_LISTEN_ADDRESS = '',
- },
- }
- clear_serverlist()
- eq({}, fn.serverlist())
-
- local s = fn.serverstart('127.0.0.1:0') -- assign random port
- if #s > 0 then
- matches('127.0.0.1:%d+', s)
- eq(s, fn.serverlist()[1])
- clear_serverlist()
- end
-
- s = fn.serverstart('127.0.0.1:') -- assign random port
- if #s > 0 then
- matches('127.0.0.1:%d+', s)
- eq(s, fn.serverlist()[1])
- clear_serverlist()
- end
-
- local expected = {}
- local v4 = '127.0.0.1:12345'
- local status, _ = pcall(fn.serverstart, v4)
- if status then
- table.insert(expected, v4)
- pcall(fn.serverstart, v4) -- exists already; ignore
- t.assert_log('Failed to start server: address already in use: 127%.0%.0%.1', testlog, 10)
- end
-
- local v6 = '::1:12345'
- status, _ = pcall(fn.serverstart, v6)
- if status then
- table.insert(expected, v6)
- pcall(fn.serverstart, v6) -- exists already; ignore
- t.assert_log('Failed to start server: address already in use: ::1', testlog, 10)
- end
- eq(expected, fn.serverlist())
- clear_serverlist()
-
- -- Address without slashes is a "name" which is appended to a generated path. #8519
- matches([[.*[/\\]xtest1%.2%.3%.4[^/\\]*]], fn.serverstart('xtest1.2.3.4'))
- clear_serverlist()
-
- eq('Vim:Failed to start server: invalid argument', pcall_err(fn.serverstart, '127.0.0.1:65536')) -- invalid port
- eq({}, fn.serverlist())
- end)
-
- it('serverlist() returns the list of servers', function()
- clear()
- -- There should already be at least one server.
- local _n = eval('len(serverlist())')
-
- -- Add some servers.
- local servs = (
- is_os('win') and { [[\\.\pipe\Xtest-pipe0934]], [[\\.\pipe\Xtest-pipe4324]] }
- or { [[./Xtest-pipe0934]], [[./Xtest-pipe4324]] }
- )
- for _, s in ipairs(servs) do
- eq(s, eval("serverstart('" .. s .. "')"))
- end
-
- local new_servs = eval('serverlist()')
-
- -- Exactly #servs servers should be added.
- eq(_n + #servs, #new_servs)
- -- The new servers should be at the end of the list.
- for i = 1, #servs do
- eq(servs[i], new_servs[i + _n])
- eq(1, eval("serverstop('" .. servs[i] .. "')"))
- end
- -- After serverstop() the servers should NOT be in the list.
- eq(_n, eval('len(serverlist())'))
- end)
-end)
-
-describe('startup --listen', function()
- -- Tests Nvim output when failing to start, with and without "--headless".
- -- TODO(justinmk): clear() should have a way to get stdout if Nvim fails to start.
- local function _test(args, env, expected)
- local function run(cmd)
- return n.exec_lua(function(cmd_, env_)
- return vim
- .system(cmd_, {
- text = true,
- env = vim.tbl_extend(
- 'force',
- -- Avoid noise in the logs; we expect failures for these tests.
- { NVIM_LOG_FILE = testlog },
- env_ or {}
- ),
- })
- :wait()
- end, cmd, env) --[[@as vim.SystemCompleted]]
- end
-
- local cmd = vim.list_extend({ n.nvim_prog, '+qall!', '--headless' }, args)
- local r = run(cmd)
- eq(1, r.code)
- matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' '))
-
- if is_os('win') then
- return -- On Windows, output without --headless is garbage.
- end
- table.remove(cmd, 3) -- Remove '--headless'.
- assert(not vim.tbl_contains(cmd, '--headless'))
- r = run(cmd)
- eq(1, r.code)
- matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' '))
- end
-
- it('validates', function()
- clear { env = { NVIM_LOG_FILE = testlog } }
- local in_use = n.eval('v:servername') ---@type string Address already used by another server.
-
- t.assert_nolog('Failed to start server', testlog, 100)
- t.assert_nolog('Host lookup failed', testlog, 100)
-
- _test({ '--listen' }, nil, 'nvim.*: Argument missing after: "%-%-listen"')
- _test({ '--listen2' }, nil, 'nvim.*: Garbage after option argument: "%-%-listen2"')
- _test(
- { '--listen', in_use },
- nil,
- ('nvim.*: Failed to %%-%%-listen: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use))
- )
- _test({ '--listen', '/' }, nil, 'nvim.*: Failed to %-%-listen: [^:]+: "/"')
- _test(
- { '--listen', 'https://example.com' },
- nil,
- ('nvim.*: Failed to %%-%%-listen: %s: "https://example.com"'):format(
- is_os('mac') and 'unknown node or service' or 'service not available for socket type'
- )
- )
-
- t.assert_log('Failed to start server', testlog, 100)
- t.assert_log('Host lookup failed', testlog, 100)
-
- _test(
- {},
- { NVIM_LISTEN_ADDRESS = in_use },
- ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use))
- )
- _test({}, { NVIM_LISTEN_ADDRESS = '/' }, 'nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+: "/"')
- _test(
- {},
- { NVIM_LISTEN_ADDRESS = 'https://example.com' },
- ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: %s: "https://example.com"'):format(
- is_os('mac') and 'unknown node or service' or 'service not available for socket type'
- )
- )
- end)
-
- it('sets v:servername, overrides $NVIM_LISTEN_ADDRESS', function()
- local addr = (is_os('win') and [[\\.\pipe\Xtest-listen-pipe]] or './Xtest-listen-pipe')
- clear({ env = { NVIM_LISTEN_ADDRESS = './Xtest-env-pipe' }, args = { '--listen', addr } })
- eq('', eval('$NVIM_LISTEN_ADDRESS')) -- Cleared on startup.
- eq(addr, api.nvim_get_vvar('servername'))
-
- -- Address without slashes is a "name" which is appended to a generated path. #8519
- clear({ args = { '--listen', 'test-name' } })
- matches([[.*[/\\]test%-name[^/\\]*]], api.nvim_get_vvar('servername'))
- end)
-end)
diff --git a/test/testutil.lua b/test/testutil.lua
@@ -16,7 +16,7 @@ local function shell_quote(str)
return str
end
---- This module uses functions from the context of the test runner.
+--- Functions executing in the context of the test runner (not the current nvim test session).
--- @class test.testutil
local M = {
paths = Paths,