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