neovim

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

testterm.lua (7270B)


      1 -- Functions to test :terminal and the Nvim TUI.
      2 -- Starts a child process in a `:terminal` and sends bytes to the child via nvim_chan_send().
      3 -- Note: the global functional/testutil.lua test-session is _host_ session, _not_
      4 -- the child session.
      5 --
      6 -- - Use `setup_screen()` to test `:terminal` behavior with an arbitrary command.
      7 -- - Use `setup_child_nvim()` to test the Nvim TUI.
      8 --    - NOTE: Only use this if your test actually needs the full lifecycle/capabilities of the
      9 --    builtin Nvim TUI. Most tests should just use `Screen.new()` directly, or plain old API calls.
     10 
     11 local t = require('test.testutil')
     12 local n = require('test.functional.testnvim')()
     13 local Screen = require('test.functional.ui.screen')
     14 
     15 local testprg = n.testprg
     16 local exec_lua = n.exec_lua
     17 local api = n.api
     18 local nvim_prog = n.nvim_prog
     19 
     20 local M = {}
     21 
     22 function M.feed_data(data)
     23  if type(data) == 'table' then
     24    data = table.concat(data, '\n')
     25  end
     26  exec_lua('vim.api.nvim_chan_send(vim.b.terminal_job_id, ...)', data)
     27 end
     28 
     29 function M.feed_termcode(data)
     30  M.feed_data('\027' .. data)
     31 end
     32 
     33 function M.feed_csi(data)
     34  M.feed_termcode('[' .. data)
     35 end
     36 
     37 --- @param session test.Session
     38 --- @return fun(code: string, ...):any
     39 function M.make_lua_executor(session)
     40  return function(code, ...)
     41    local status, rv = session:request('nvim_exec_lua', code, { ... })
     42    if not status then
     43      session:stop()
     44      error(rv[2])
     45    end
     46    return rv
     47  end
     48 end
     49 
     50 -- some helpers for controlling the terminal. the codes were taken from
     51 -- infocmp xterm-256color which is less what libvterm understands
     52 -- civis/cnorm
     53 function M.hide_cursor()
     54  M.feed_termcode('[?25l')
     55 end
     56 function M.show_cursor()
     57  M.feed_termcode('[?25h')
     58 end
     59 -- smcup/rmcup
     60 function M.enter_altscreen()
     61  M.feed_termcode('[?1049h')
     62 end
     63 function M.exit_altscreen()
     64  M.feed_termcode('[?1049l')
     65 end
     66 -- character attributes
     67 function M.set_fg(num)
     68  M.feed_termcode('[38;5;' .. num .. 'm')
     69 end
     70 function M.set_bg(num)
     71  M.feed_termcode('[48;5;' .. num .. 'm')
     72 end
     73 function M.set_bold()
     74  M.feed_termcode('[1m')
     75 end
     76 function M.set_italic()
     77  M.feed_termcode('[3m')
     78 end
     79 function M.set_underline()
     80  M.feed_termcode('[4m')
     81 end
     82 function M.set_underdouble()
     83  M.feed_termcode('[4:2m')
     84 end
     85 function M.set_undercurl()
     86  M.feed_termcode('[4:3m')
     87 end
     88 function M.set_reverse()
     89  M.feed_termcode('[7m')
     90 end
     91 function M.set_strikethrough()
     92  M.feed_termcode('[9m')
     93 end
     94 function M.clear_attrs()
     95  M.feed_termcode('[0;10m')
     96 end
     97 -- mouse
     98 function M.enable_mouse()
     99  M.feed_termcode('[?1002h')
    100 end
    101 function M.disable_mouse()
    102  M.feed_termcode('[?1002l')
    103 end
    104 
    105 local default_command = { testprg('tty-test') }
    106 
    107 --- Runs `cmd` in a :terminal, and returns a `Screen` object.
    108 ---
    109 ---@param extra_rows? integer Extra rows to add to the default screen.
    110 ---@param cmd? string|string[] Command to run in the terminal (default: `{ 'tty-test' }`)
    111 ---@param cols? integer Create screen with this many columns (default: 50)
    112 ---@param env? table Environment set on the `cmd` job.
    113 ---@param screen_opts? table Options for `Screen.new()`.
    114 ---@return test.functional.ui.screen # Screen attached to the global (not child) Nvim session.
    115 function M.setup_screen(extra_rows, cmd, cols, env, screen_opts)
    116  extra_rows = extra_rows and extra_rows or 0
    117  cmd = cmd and cmd or default_command
    118  cols = cols and cols or 50
    119 
    120  api.nvim_command('highlight StatusLineTerm ctermbg=2 ctermfg=0')
    121  api.nvim_command('highlight StatusLineTermNC ctermbg=2 ctermfg=8')
    122 
    123  local screen = Screen.new(cols, 7 + extra_rows, screen_opts or { rgb = false })
    124  screen:add_extra_attr_ids({
    125    [100] = { foreground = 12 },
    126    [101] = { foreground = 15, background = 1 },
    127    [102] = { foreground = 121 },
    128    [103] = { foreground = 11 },
    129    [104] = { foreground = 81 },
    130    [105] = { underline = true, reverse = true },
    131    [106] = { underline = true, reverse = true, bold = true },
    132    [107] = { underline = true },
    133    [108] = { background = 248, foreground = Screen.colors.Black },
    134    [109] = { bold = true, background = 121, foreground = Screen.colors.Grey0 },
    135    [110] = { fg_indexed = true, foreground = tonumber('0xe0e000') },
    136    [111] = { fg_indexed = true, foreground = tonumber('0x4040ff') },
    137    [112] = { foreground = 4 },
    138    [113] = { foreground = Screen.colors.SeaGreen4 },
    139    [114] = { undercurl = true },
    140    [115] = { underdouble = true },
    141    [116] = { underline = true, foreground = 12 },
    142    [117] = { background = 1 },
    143    [118] = { background = 1, reverse = true },
    144    [119] = { background = 2, foreground = 8 },
    145    [120] = { foreground = Screen.colors.Black, background = 2 },
    146    [121] = { foreground = 130 },
    147    [122] = { background = 46 },
    148    [123] = { foreground = 2 },
    149  })
    150 
    151  api.nvim_command('enew')
    152  api.nvim_call_function('jobstart', { cmd, { term = true, env = (env and env or nil) } })
    153  api.nvim_input('<CR>')
    154  local vim_errmsg = api.nvim_eval('v:errmsg')
    155  if vim_errmsg and '' ~= vim_errmsg then
    156    error(vim_errmsg)
    157  end
    158 
    159  api.nvim_command('setlocal scrollback=10')
    160  api.nvim_command('startinsert')
    161  api.nvim_input('<Ignore>') -- Add input to separate two RPC requests
    162 
    163  -- tty-test puts the terminal into raw mode and echoes input. Tests work by
    164  -- feeding termcodes to control the display and asserting by screen:expect.
    165  if cmd == default_command and screen_opts == nil then
    166    -- Wait for "tty ready" to be printed before each test or the terminal may
    167    -- still be in canonical mode (will echo characters for example).
    168    local empty_line = (' '):rep(cols)
    169    local expected = {
    170      'tty ready' .. (' '):rep(cols - 9),
    171      '^' .. (' '):rep(cols),
    172      empty_line,
    173      empty_line,
    174      empty_line,
    175      empty_line,
    176    }
    177    for _ = 1, extra_rows do
    178      table.insert(expected, empty_line)
    179    end
    180 
    181    table.insert(expected, '{5:-- TERMINAL --}' .. ((' '):rep(cols - 14)))
    182    screen:expect(table.concat(expected, '|\n') .. '|')
    183  else
    184    -- This eval also acts as a poke_eventloop().
    185    if 0 == api.nvim_eval("exists('b:terminal_job_id')") then
    186      error('terminal job failed to start')
    187    end
    188  end
    189  return screen
    190 end
    191 
    192 --- Spawns Nvim with `args` in a :terminal, and returns a `Screen` object.
    193 ---
    194 --- @note Only use this if you actually need the full lifecycle/capabilities of the builtin Nvim
    195 --- TUI. Most tests should just use `Screen.new()` directly, or plain old API calls.
    196 ---
    197 ---@param args? string[] Args passed to child Nvim.
    198 ---@param opts? table Options
    199 ---@return test.functional.ui.screen # Screen attached to the global (not child) Nvim session.
    200 function M.setup_child_nvim(args, opts)
    201  opts = opts or {}
    202  local argv = { nvim_prog, unpack(args or {}) }
    203 
    204  local env = opts.env or {}
    205  env.VIMRUNTIME = env.VIMRUNTIME or os.getenv('VIMRUNTIME')
    206  env.NVIM_TEST = env.NVIM_TEST or os.getenv('NVIM_TEST')
    207 
    208  return M.setup_screen(opts.extra_rows, argv, opts.cols, env)
    209 end
    210 
    211 --- FIXME: On Windows spaces at the end of a screen line may have wrong attrs.
    212 --- Remove this function when that's fixed.
    213 ---
    214 --- @param screen test.functional.ui.screen
    215 --- @param s string
    216 function M.screen_expect(screen, s)
    217  if t.is_os('win') then
    218    s = s:gsub(' *%} +%|\n', '{MATCH: *}}{MATCH: *}|\n')
    219    s = s:gsub('%}%^ +%|\n', '{MATCH:[ ^]*}}{MATCH:[ ^]*}|\n')
    220  end
    221  screen:expect(s)
    222 end
    223 
    224 return M