1local helpers = require('test.unit.helpers')(after_each)
2local itp = helpers.gen_itp(it)
3
4local cimport = helpers.cimport
5local eq = helpers.eq
6local neq = helpers.neq
7local ffi = helpers.ffi
8local cstr = helpers.cstr
9local to_cstr = helpers.to_cstr
10local NULL = helpers.NULL
11local OK = 0
12
13require('lfs')
14
15local cimp = cimport('./src/nvim/os/os.h')
16
17describe('env.c', function()
18  local function os_env_exists(name)
19    return cimp.os_env_exists(to_cstr(name))
20  end
21
22  local function os_setenv(name, value, override)
23    return cimp.os_setenv(to_cstr(name), to_cstr(value), override)
24  end
25
26  local function os_unsetenv(name)
27    return cimp.os_unsetenv(to_cstr(name))
28  end
29
30  local function os_getenv(name)
31    local rval = cimp.os_getenv(to_cstr(name))
32    if rval ~= NULL then
33      return ffi.string(rval)
34    else
35      return NULL
36    end
37  end
38
39  itp('os_env_exists', function()
40    eq(false, os_env_exists(''))
41    eq(false, os_env_exists('      '))
42    eq(false, os_env_exists('\t'))
43    eq(false, os_env_exists('\n'))
44    eq(false, os_env_exists('AaあB <= very weird name...'))
45
46    local varname = 'NVIM_UNIT_TEST_os_env_exists'
47    eq(false, os_env_exists(varname))
48    eq(OK, os_setenv(varname, 'foo bar baz ...', 1))
49    eq(true, os_env_exists(varname))
50  end)
51
52  describe('os_setenv', function()
53    itp('sets an env var and returns success', function()
54      local name = 'NVIM_UNIT_TEST_SETENV_1N'
55      local value = 'NVIM_UNIT_TEST_SETENV_1V'
56      eq(nil, os.getenv(name))
57      eq(OK, os_setenv(name, value, 1))
58      eq(value, os.getenv(name))
59
60      -- Set empty, then set non-empty, then retrieve.
61      eq(OK, os_setenv(name, '', 1))
62      eq('', os.getenv(name))
63      eq(OK, os_setenv(name, 'non-empty', 1))
64      eq('non-empty', os.getenv(name))
65    end)
66
67    itp("`overwrite` behavior", function()
68      local name = 'NVIM_UNIT_TEST_SETENV_2N'
69      local value = 'NVIM_UNIT_TEST_SETENV_2V'
70      local value_updated = 'NVIM_UNIT_TEST_SETENV_2V_UPDATED'
71      eq(OK, os_setenv(name, value, 0))
72      eq(value, os.getenv(name))
73      eq(OK, os_setenv(name, value_updated, 0))
74      eq(value, os.getenv(name))
75      eq(OK, os_setenv(name, value_updated, 1))
76      eq(value_updated, os.getenv(name))
77    end)
78  end)
79
80  describe('os_setenv_append_path', function()
81    itp('appends :/foo/bar to $PATH', function()
82      local original_path = os.getenv('PATH')
83      eq(true, cimp.os_setenv_append_path(to_cstr('/foo/bar/baz.exe')))
84      eq(original_path..':/foo/bar', os.getenv('PATH'))
85    end)
86
87    itp('avoids redundant separator when appending to $PATH #7377', function()
88      os_setenv('PATH', '/a/b/c:', true)
89      eq(true, cimp.os_setenv_append_path(to_cstr('/foo/bar/baz.exe')))
90      -- Must not have duplicate separators. #7377
91      eq('/a/b/c:/foo/bar', os.getenv('PATH'))
92    end)
93
94    itp('returns false if `fname` is not absolute', function()
95      local original_path = os.getenv('PATH')
96      eq(false, cimp.os_setenv_append_path(to_cstr('foo/bar/baz.exe')))
97      eq(original_path, os.getenv('PATH'))
98    end)
99  end)
100
101  describe('os_shell_is_cmdexe', function()
102    itp('returns true for expected names', function()
103      eq(true, cimp.os_shell_is_cmdexe(to_cstr('cmd.exe')))
104      eq(true, cimp.os_shell_is_cmdexe(to_cstr('cmd')))
105      eq(true, cimp.os_shell_is_cmdexe(to_cstr('CMD.EXE')))
106      eq(true, cimp.os_shell_is_cmdexe(to_cstr('CMD')))
107
108      os_setenv('COMSPEC', '/foo/bar/cmd.exe', 0)
109      eq(true, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC')))
110      os_setenv('COMSPEC', [[C:\system32\cmd.exe]], 0)
111      eq(true, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC')))
112    end)
113    itp('returns false for unexpected names', function()
114      eq(false, cimp.os_shell_is_cmdexe(to_cstr('')))
115      eq(false, cimp.os_shell_is_cmdexe(to_cstr('powershell')))
116      eq(false, cimp.os_shell_is_cmdexe(to_cstr(' cmd.exe ')))
117      eq(false, cimp.os_shell_is_cmdexe(to_cstr('cm')))
118      eq(false, cimp.os_shell_is_cmdexe(to_cstr('md')))
119      eq(false, cimp.os_shell_is_cmdexe(to_cstr('cmd.ex')))
120
121      os_setenv('COMSPEC', '/foo/bar/cmd', 0)
122      eq(false, cimp.os_shell_is_cmdexe(to_cstr('$COMSPEC')))
123    end)
124  end)
125
126  describe('os_getenv', function()
127    itp('reads an env var', function()
128      local name = 'NVIM_UNIT_TEST_GETENV_1N'
129      local value = 'NVIM_UNIT_TEST_GETENV_1V'
130      eq(NULL, os_getenv(name))
131      -- Use os_setenv because Lua doesn't have setenv.
132      os_setenv(name, value, 1)
133      eq(value, os_getenv(name))
134
135      -- Get a big value.
136      local bigval = ('x'):rep(256)
137      eq(OK, os_setenv(name, bigval, 1))
138      eq(bigval, os_getenv(name))
139
140      -- Set non-empty, then set empty.
141      eq(OK, os_setenv(name, 'non-empty', 1))
142      eq('non-empty', os_getenv(name))
143      eq(OK, os_setenv(name, '', 1))
144      eq(NULL, os_getenv(name))
145    end)
146
147    itp('returns NULL if the env var is not found', function()
148      eq(NULL, os_getenv('NVIM_UNIT_TEST_GETENV_NOTFOUND'))
149    end)
150  end)
151
152  itp('os_unsetenv', function()
153    local name = 'TEST_UNSETENV'
154    local value = 'TESTVALUE'
155    os_setenv(name, value, 1)
156    eq(OK, os_unsetenv(name))
157    neq(os_getenv(name), value)
158    -- Depending on the platform the var might be unset or set as ''
159    assert.True(os_getenv(name) == nil or os_getenv(name) == '')
160    if os_getenv(name) == nil then
161      eq(false, os_env_exists(name))
162    end
163  end)
164
165  describe('os_getenvname_at_index', function()
166    itp('returns names of environment variables', function()
167      local test_name = 'NVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1N'
168      local test_value = 'NVIM_UNIT_TEST_GETENVNAME_AT_INDEX_1V'
169      os_setenv(test_name, test_value, 1)
170      local i = 0
171      local names = { }
172      local found_name = false
173      local name = cimp.os_getenvname_at_index(i)
174      while name ~= NULL do
175        table.insert(names, ffi.string(name))
176        if (ffi.string(name)) == test_name then
177          found_name = true
178        end
179        i = i + 1
180        name = cimp.os_getenvname_at_index(i)
181      end
182      eq(true, #names > 0)
183      eq(true, found_name)
184    end)
185
186    itp('returns NULL if the index is out of bounds', function()
187      local huge = ffi.new('size_t', 10000)
188      local maxuint32 = ffi.new('size_t', 4294967295)
189      eq(NULL, cimp.os_getenvname_at_index(huge))
190      eq(NULL, cimp.os_getenvname_at_index(maxuint32))
191
192      if ffi.abi('64bit') then
193        -- couldn't use a bigger number because it gets converted to
194        -- double somewere, should be big enough anyway
195        -- maxuint64 = ffi.new 'size_t', 18446744073709551615
196        local maxuint64 = ffi.new('size_t', 18446744073709000000)
197        eq(NULL, cimp.os_getenvname_at_index(maxuint64))
198      end
199    end)
200  end)
201
202  describe('os_get_pid', function()
203    itp('returns the process ID', function()
204      local stat_file = io.open('/proc/self/stat')
205      if stat_file then
206        local stat_str = stat_file:read('*l')
207        stat_file:close()
208        local pid = tonumber((stat_str:match('%d+')))
209        eq(pid, tonumber(cimp.os_get_pid()))
210      else
211        -- /proc is not available on all systems, test if pid is nonzero.
212        eq(true, (cimp.os_get_pid() > 0))
213      end
214    end)
215  end)
216
217  describe('os_get_hostname', function()
218    itp('returns the hostname', function()
219      local handle = io.popen('hostname')
220      local hostname = handle:read('*l')
221      handle:close()
222      local hostname_buf = cstr(255, '')
223      cimp.os_get_hostname(hostname_buf, 255)
224      eq(hostname, (ffi.string(hostname_buf)))
225    end)
226  end)
227
228  describe('expand_env_esc', function()
229    itp('expands environment variables', function()
230      local name = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCN'
231      local value = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCV'
232      os_setenv(name, value, 1)
233      -- TODO(bobtwinkles) This only tests Unix expansions. There should be a
234      -- test for Windows as well
235      local input1 = to_cstr('$NVIM_UNIT_TEST_EXPAND_ENV_ESCN/test')
236      local input2 = to_cstr('${NVIM_UNIT_TEST_EXPAND_ENV_ESCN}/test')
237      local output_buff1 = cstr(255, '')
238      local output_buff2 = cstr(255, '')
239      local output_expected = 'NVIM_UNIT_TEST_EXPAND_ENV_ESCV/test'
240      cimp.expand_env_esc(input1, output_buff1, 255, false, true, NULL)
241      cimp.expand_env_esc(input2, output_buff2, 255, false, true, NULL)
242      eq(output_expected, ffi.string(output_buff1))
243      eq(output_expected, ffi.string(output_buff2))
244    end)
245
246    itp('expands ~ once when `one` is true', function()
247      local input = '~/foo ~ foo'
248      local homedir = cstr(255, '')
249      cimp.expand_env_esc(to_cstr('~'), homedir, 255, false, true, NULL)
250      local output_expected = ffi.string(homedir) .. "/foo ~ foo"
251      local output = cstr(255, '')
252      cimp.expand_env_esc(to_cstr(input), output, 255, false, true, NULL)
253      eq(ffi.string(output), ffi.string(output_expected))
254    end)
255
256    itp('expands ~ every time when `one` is false', function()
257      local input = to_cstr('~/foo ~ foo')
258      local dst = cstr(255, '')
259      cimp.expand_env_esc(to_cstr('~'), dst, 255, false, true, NULL)
260      local homedir = ffi.string(dst)
261      local output_expected = homedir .. "/foo " .. homedir .. " foo"
262      local output = cstr(255, '')
263      cimp.expand_env_esc(input, output, 255, false, false, NULL)
264      eq(output_expected, ffi.string(output))
265    end)
266
267    itp('does not crash #3725', function()
268      local name_out = ffi.new('char[100]')
269      cimp.os_get_user_name(name_out, 100)
270      local curuser = ffi.string(name_out)
271
272      local src = to_cstr("~"..curuser.."/Vcs/django-rest-framework/rest_framework/renderers.py")
273      local dst = cstr(256, "~"..curuser)
274      cimp.expand_env_esc(src, dst, 256, false, false, NULL)
275      local len = string.len(ffi.string(dst))
276      assert.True(len > 56)
277      assert.True(len < 256)
278    end)
279
280    itp('respects `dstlen` without expansion', function()
281      local input = to_cstr('this is a very long thing that will not fit')
282      -- The buffer is long enough to actually contain the full input in case the
283      -- test fails, but we don't tell expand_env_esc that
284      local output = cstr(255, '')
285      cimp.expand_env_esc(input, output, 5, false, true, NULL)
286      -- Make sure the first few characters are copied properly and that there is a
287      -- terminating null character
288      for i=0,3 do
289        eq(input[i], output[i])
290      end
291      eq(0, output[4])
292    end)
293
294    itp('respects `dstlen` with expansion', function()
295      local varname = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN')
296      local varval = to_cstr('NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENV')
297      cimp.os_setenv(varname, varval, 1)
298      -- TODO(bobtwinkles) This test uses unix-specific environment variable accessing,
299      -- should have some alternative for windows
300      local input = to_cstr('$NVIM_UNIT_TEST_EXPAND_ENV_ESC_DSTLENN/even more stuff')
301      -- The buffer is long enough to actually contain the full input in case the
302      -- test fails, but we don't tell expand_env_esc that
303      local output = cstr(255, '')
304      cimp.expand_env_esc(input, output, 5, false, true, NULL)
305      -- Make sure the first few characters are copied properly and that there is a
306      -- terminating null character
307      -- expand_env_esc SHOULD NOT expand the variable if there is not enough space to
308      -- contain the result
309      for i=0,3 do
310        eq(output[i], input[i])
311      end
312      eq(output[4], 0)
313    end)
314  end)
315end)
316