diff options
| author | dundargoc <gocdundar@gmail.com> | 2024-09-25 16:07:37 +0200 |
|---|---|---|
| committer | dundargoc <33953936+dundargoc@users.noreply.github.com> | 2024-10-22 21:20:11 +0200 |
| commit | 4c25e60767508a3f8bc0631d280407a08f24a67e (patch) | |
| tree | 1de749c20df1b169557efd44373f4764f511bca1 /test | |
| parent | 07b4cb6ada2da108bbf8277bec63068234ceaa67 (diff) | |
| download | rneovim-4c25e60767508a3f8bc0631d280407a08f24a67e.tar.gz rneovim-4c25e60767508a3f8bc0631d280407a08f24a67e.tar.bz2 rneovim-4c25e60767508a3f8bc0631d280407a08f24a67e.zip | |
test: port libvterm unit test to neovim test suite
These were imported from the v0.3.3 git tag
https://github.com/neovim/libvterm/tree/v0.3.3 and not the latest
commit. This is for compatibility reasons as the libvterm code was
imported from v0.3.3.
Diffstat (limited to 'test')
| -rw-r--r-- | test/cmakeconfig/paths.lua.in | 1 | ||||
| -rw-r--r-- | test/unit/testutil.lua | 7 | ||||
| -rw-r--r-- | test/unit/vterm_spec.lua | 3591 |
3 files changed, 3599 insertions, 0 deletions
diff --git a/test/cmakeconfig/paths.lua.in b/test/cmakeconfig/paths.lua.in index ce0eb870e0..78c66a4eac 100644 --- a/test/cmakeconfig/paths.lua.in +++ b/test/cmakeconfig/paths.lua.in @@ -5,6 +5,7 @@ for p in ("${TEST_INCLUDE_DIRS}" .. ";"):gmatch("[^;]+") do table.insert(M.include_paths, p) end +M.vterm_test_file = "${VTERM_TEST_FILE}" M.test_build_dir = "${CMAKE_BINARY_DIR}" M.test_source_path = "${CMAKE_SOURCE_DIR}" M.test_lua_prg = "${LUA_PRG}" diff --git a/test/unit/testutil.lua b/test/unit/testutil.lua index a6db7beab1..4720d4d730 100644 --- a/test/unit/testutil.lua +++ b/test/unit/testutil.lua @@ -151,6 +151,13 @@ local function filter_complex_blocks(body) or string.find(line, 'mach_vm_range_recipe') ) then + -- HACK: remove bitfields from specific structs as luajit can't seem to handle them. + if line:find('struct VTermState') then + line = string.gsub(line, 'state : 8;', 'state;') + end + if line:find('VTermStringFragment') then + line = string.gsub(line, 'size_t.*len : 30;', 'size_t len;') + end result[#result + 1] = line end end diff --git a/test/unit/vterm_spec.lua b/test/unit/vterm_spec.lua new file mode 100644 index 0000000000..f0cf450e6c --- /dev/null +++ b/test/unit/vterm_spec.lua @@ -0,0 +1,3591 @@ +local t = require('test.unit.testutil') +local itp = t.gen_itp(it) +local bit = require('bit') + +--- @class vterm +--- @field ENC_UTF8 integer +--- @field VTERM_ATTR_BLINK integer +--- @field VTERM_ATTR_BOLD integer +--- @field VTERM_ATTR_FONT integer +--- @field VTERM_ATTR_ITALIC integer +--- @field VTERM_ATTR_REVERSE integer +--- @field VTERM_ATTR_UNDERLINE integer +--- @field VTERM_BASELINE_RAISE integer +--- @field VTERM_KEY_ENTER integer +--- @field VTERM_KEY_FUNCTION_0 integer +--- @field VTERM_KEY_KP_0 integer +--- @field VTERM_KEY_NONE integer +--- @field VTERM_KEY_TAB integer +--- @field VTERM_KEY_UP integer +--- @field VTERM_MAX_CHARS_PER_CELL integer +--- @field VTERM_MOD_ALT integer +--- @field VTERM_MOD_CTRL integer +--- @field VTERM_MOD_SHIFT integer +--- @field parser_apc function +--- @field parser_csi function +--- @field parser_dcs function +--- @field parser_osc function +--- @field parser_pm function +--- @field parser_sos function +--- @field parser_text function +--- @field print_color function +--- @field screen_sb_clear function +--- @field screen_sb_popline function +--- @field screen_sb_pushline function +--- @field selection_query function +--- @field selection_set function +--- @field state_erase function +--- @field state_movecursor function +--- @field state_moverect function +--- @field state_pos function +--- @field state_putglyph function +--- @field state_sb_clear function +--- @field state_scrollrect function +--- @field state_setpenattr function +--- @field state_settermprop function +--- @field term_output function +--- @field vterm_input_write function +--- @field vterm_keyboard_end_paste function +--- @field vterm_keyboard_key function +--- @field vterm_keyboard_start_paste function +--- @field vterm_keyboard_unichar function +--- @field vterm_lookup_encoding fun(any, any):any +--- @field vterm_mouse_button function +--- @field vterm_mouse_move function +--- @field vterm_new fun(any, any):any +--- @field vterm_obtain_screen fun(any):any +--- @field vterm_obtain_state fun(any): any +--- @field vterm_output_set_callback function +--- @field vterm_parser_set_callbacks fun(any, any, any):any +--- @field vterm_screen_convert_color_to_rgb function +--- @field vterm_screen_enable_altscreen function +--- @field vterm_screen_enable_reflow function +--- @field vterm_screen_get_attrs_extent function +--- @field vterm_screen_get_cell function +--- @field vterm_screen_get_chars fun(any, any, any, any):any +--- @field vterm_screen_get_text fun(any, any, any, any):any +--- @field vterm_screen_is_eol fun(any, any):any +--- @field vterm_screen_reset function +--- @field vterm_screen_set_callbacks function +--- @field vterm_set_size function +--- @field vterm_set_utf8 fun(any, any, any):any +--- @field vterm_state_focus_in function +--- @field vterm_state_focus_out function +--- @field vterm_state_get_cursorpos fun(any, any) +--- @field vterm_state_get_lineinfo fun(any, any):any +--- @field vterm_state_get_penattr function +--- @field vterm_state_reset function +--- @field vterm_state_set_bold_highbright function +--- @field vterm_state_set_callbacks function +--- @field vterm_state_set_selection_callbacks function +--- @field vterm_state_set_unrecognised_fallbacks function +local vterm = t.cimport('./src/vterm/vterm.h', './src/vterm/vterm_internal.h') + +--- @return string +local function read_rm() + local f = assert(io.open(t.paths.vterm_test_file, 'rb')) + local text = f:read('*a') + f:close() + vim.fs.rm(t.paths.vterm_test_file, { force = true }) + return text +end + +local function append(str) + local f = assert(io.open(t.paths.vterm_test_file, 'a')) + f:write(str) + f:close() + return 1 +end + +local function parser_control(control) + return append(string.format('control %02x\n', control)) +end + +local function parser_escape(bytes) + return append(string.format('escape %s\n', t.ffi.string(bytes))) +end + +local function wantparser(vt) + assert(vt) + + local parser_cbs = t.ffi.new('VTermParserCallbacks') + parser_cbs['text'] = vterm.parser_text + parser_cbs['control'] = parser_control + parser_cbs['escape'] = parser_escape + parser_cbs['csi'] = vterm.parser_csi + parser_cbs['osc'] = vterm.parser_osc + parser_cbs['dcs'] = vterm.parser_dcs + parser_cbs['apc'] = vterm.parser_apc + parser_cbs['pm'] = vterm.parser_pm + parser_cbs['sos'] = vterm.parser_sos + + vterm.vterm_parser_set_callbacks(vt, parser_cbs, nil) +end + +--- @return any +local function init() + local vt = vterm.vterm_new(25, 80) + vterm.vterm_output_set_callback(vt, vterm.term_output, nil) + vterm.vterm_set_utf8(vt, true) + return vt +end + +local function state_setlineinfo() + return 1 +end + +--- @return any +local function wantstate(vt, opts) + opts = opts or {} + assert(vt) + local state = vterm.vterm_obtain_state(vt) + + local state_cbs = t.ffi.new('VTermStateCallbacks') + state_cbs['putglyph'] = vterm.state_putglyph + state_cbs['movecursor'] = vterm.state_movecursor + state_cbs['scrollrect'] = vterm.state_scrollrect + state_cbs['moverect'] = vterm.state_moverect + state_cbs['erase'] = vterm.state_erase + state_cbs['setpenattr'] = vterm.state_setpenattr + state_cbs['settermprop'] = vterm.state_settermprop + state_cbs['setlineinfo'] = state_setlineinfo + state_cbs['sb_clear'] = vterm.state_sb_clear + + local selection_cbs = t.ffi.new('VTermSelectionCallbacks') + selection_cbs['set'] = vterm.selection_set + selection_cbs['query'] = vterm.selection_query + + vterm.vterm_state_set_callbacks(state, state_cbs, nil) + + -- In some tests we want to check the behaviour of overflowing the buffer, so make it nicely small + vterm.vterm_state_set_selection_callbacks(state, selection_cbs, nil, nil, 16) + vterm.vterm_state_set_bold_highbright(state, 1) + vterm.vterm_state_reset(state, 1) + + local fallbacks = t.ffi.new('VTermStateFallbacks') + fallbacks['control'] = parser_control + fallbacks['csi'] = vterm.parser_csi + fallbacks['osc'] = vterm.parser_osc + fallbacks['dcs'] = vterm.parser_dcs + fallbacks['apc'] = vterm.parser_apc + fallbacks['pm'] = vterm.parser_pm + fallbacks['sos'] = vterm.parser_sos + + vterm.want_state_scrollback = opts.b or false + vterm.want_state_erase = opts.e or false + vterm.vterm_state_set_unrecognised_fallbacks(state, opts.f and fallbacks or nil, nil) + vterm.want_state_putglyph = opts.g or false + vterm.want_state_moverect = opts.m or false + vterm.want_state_settermprop = opts.p or false + vterm.want_state_scrollrect = opts.s or false + + return state +end + +--- @return any +local function wantscreen(vt, opts) + opts = opts or {} + local screen = vterm.vterm_obtain_screen(vt) + local screen_cbs = t.ffi.new('VTermScreenCallbacks') + + -- TODO(dundargoc): fix + -- screen_cbs['damage'] = vterm.screen_damage + screen_cbs['moverect'] = vterm.state_moverect + screen_cbs['movecursor'] = vterm.state_movecursor + screen_cbs['settermprop'] = vterm.state_settermprop + screen_cbs['sb_pushline'] = vterm.screen_sb_pushline + screen_cbs['sb_popline'] = vterm.screen_sb_popline + screen_cbs['sb_clear'] = vterm.screen_sb_clear + + vterm.vterm_screen_set_callbacks(screen, screen_cbs, nil) + + if opts.a then + vterm.vterm_screen_enable_altscreen(screen, 1) + end + vterm.want_screen_scrollback = opts.b or false + vterm.want_state_movecursor = opts.c or false + -- TODO(dundargoc): fix + -- vterm.want_screen_damage = opts.d or opts.D or false + -- vterm.want_screen_cells = opts.D or false + vterm.want_state_moverect = opts.m or false + vterm.want_state_settermprop = opts.p or false + if opts.r then + vterm.vterm_screen_enable_reflow(screen, true) + end + + return screen +end + +local function reset(state, screen) + if state then + vterm.vterm_state_reset(state, 1) + vterm.vterm_state_get_cursorpos(state, vterm.state_pos) + end + if screen then + vterm.vterm_screen_reset(screen, 1) + end +end + +local function push(input, vt) + vterm.vterm_input_write(vt, input, string.len(input)) +end + +local function expect(expected) + local actual = read_rm() + t.eq(expected .. '\n', actual) +end + +local function expect_output(expected_preformat) + local actual = read_rm() + local expected = 'output ' + + for c in string.gmatch(expected_preformat, '.') do + if expected ~= 'output ' then + expected = expected .. ',' + end + expected = string.format('%s%x', expected, string.byte(c)) + end + + t.eq(expected .. '\n', actual) +end + +local function cursor(row, col, state) + local pos = t.ffi.new('VTermPos') --- @type {row: integer, col: integer} + vterm.vterm_state_get_cursorpos(state, pos) + t.eq(row, pos.row) + t.eq(col, pos.col) +end + +local function lineinfo(row, expected, state) + local info = vterm.vterm_state_get_lineinfo(state, row) + local dwl = info.doublewidth == 1 + local dhl = info.doubleheight == 1 + local cont = info.continuation == 1 + + t.eq(dwl, expected.dwl or false) + t.eq(dhl, expected.dhl or false) + t.eq(cont, expected.cont or false) +end + +local function pen(attribute, expected, state) + local is_bool = { bold = true, italic = true, blink = true, reverse = true } + local vterm_attribute = { + bold = vterm.VTERM_ATTR_BOLD, + underline = vterm.VTERM_ATTR_UNDERLINE, + italic = vterm.VTERM_ATTR_ITALIC, + blink = vterm.VTERM_ATTR_BLINK, + reverse = vterm.VTERM_ATTR_REVERSE, + font = vterm.VTERM_ATTR_FONT, + } + + local val = t.ffi.new('VTermValue') --- @type {boolean: integer} + vterm.vterm_state_get_penattr(state, vterm_attribute[attribute], val) + local actual = val.boolean --- @type integer|boolean + if is_bool[attribute] then + actual = val.boolean == 1 + end + t.eq(expected, actual) +end + +local function resize(rows, cols, vt) + vterm.vterm_set_size(vt, rows, cols) +end + +local function screen_chars(start_row, start_col, end_row, end_col, expected, screen) + local rect = t.ffi.new('VTermRect') + rect['start_row'] = start_row + rect['start_col'] = start_col + rect['end_row'] = end_row + rect['end_col'] = end_col + + local len = vterm.vterm_screen_get_chars(screen, nil, 0, rect) + + local chars = t.ffi.new('uint32_t[?]', len) + vterm.vterm_screen_get_chars(screen, chars, len, rect) + + local actual = '' + for i = 0, tonumber(len) - 1 do + actual = actual .. string.char(chars[i]) + end + + t.eq(expected, actual) +end + +local function screen_text(start_row, start_col, end_row, end_col, expected, screen) + local rect = t.ffi.new('VTermRect') + rect['start_row'] = start_row + rect['start_col'] = start_col + rect['end_row'] = end_row + rect['end_col'] = end_col + + local len = vterm.vterm_screen_get_text(screen, nil, 0, rect) + + local text = t.ffi.new('unsigned char[?]', len) + vterm.vterm_screen_get_text(screen, text, len, rect) + + local actual = '' + for i = 0, tonumber(len) - 1 do + actual = string.format('%s%02x,', actual, text[i]) + end + actual = actual:sub(1, -2) + + t.eq(expected, actual) +end + +--- @param row integer +local function screen_row(row, expected, screen, end_col) + local rect = t.ffi.new('VTermRect') + rect['start_row'] = row + rect['start_col'] = 0 + rect['end_row'] = row + 1 + rect['end_col'] = end_col or 80 + + local len = vterm.vterm_screen_get_text(screen, nil, 0, rect) + + local text = t.ffi.new('unsigned char[?]', len) + vterm.vterm_screen_get_text(screen, text, len, rect) + + t.eq(expected, t.ffi.string(text)) +end + +local function screen_cell(row, col, expected, screen) + local pos = t.ffi.new('VTermPos') + pos['row'] = row + pos['col'] = col + + local cell = t.ffi.new('VTermScreenCell') + vterm.vterm_screen_get_cell(screen, pos, cell) + + local actual = '{' + for i = 0, vterm.VTERM_MAX_CHARS_PER_CELL - 1 do + if cell['chars'][i] ~= 0 then + if i > 0 then + actual = actual .. ',' + end + actual = string.format('%s%02x', actual, cell['chars'][i]) + end + end + actual = string.format('%s} width=%d attrs={', actual, cell['width']) + actual = actual .. (cell['attrs'].bold ~= 0 and 'B' or '') + actual = actual + .. (cell['attrs'].underline ~= 0 and string.format('U%d', cell['attrs'].underline) or '') + actual = actual .. (cell['attrs'].italic ~= 0 and 'I' or '') + actual = actual .. (cell['attrs'].blink ~= 0 and 'K' or '') + actual = actual .. (cell['attrs'].reverse ~= 0 and 'R' or '') + actual = actual .. (cell['attrs'].font ~= 0 and string.format('F%d', cell['attrs'].font) or '') + actual = actual .. (cell['attrs'].small ~= 0 and 'S' or '') + if cell['attrs'].baseline ~= 0 then + actual = actual .. (cell['attrs'].baseline == vterm.VTERM_BASELINE_RAISE and '^' or '_') + end + actual = actual .. '} ' + + actual = actual .. (cell['attrs'].dwl ~= 0 and 'dwl ' or '') + if cell['attrs'].dhl ~= 0 then + actual = actual .. string.format('dhl-%s ', cell['attrs'].dhl == 2 and 'bottom' or 'top') + end + + actual = string.format('%sfg=', actual) + vterm.vterm_screen_convert_color_to_rgb(screen, cell['fg']) + vterm.print_color(cell['fg']) + + actual = actual .. read_rm() + actual = actual .. ' bg=' + + vterm.vterm_screen_convert_color_to_rgb(screen, cell['bg']) + vterm.print_color(cell['bg']) + + actual = actual .. read_rm() + + t.eq(expected, actual) +end + +local function screen_eol(row, col, expected, screen) + local pos = t.ffi.new('VTermPos') + pos['row'] = row + pos['col'] = col + + local is_eol = vterm.vterm_screen_is_eol(screen, pos) + t.eq(expected, is_eol) +end + +local function screen_attrs_extent(row, col, expected, screen) + local pos = t.ffi.new('VTermPos') + pos['row'] = row + pos['col'] = col + + local rect = t.ffi.new('VTermRect') + rect['start_col'] = 0 + rect['end_col'] = -1 + vterm.vterm_screen_get_attrs_extent(screen, rect, pos, 1) + + local actual = string.format( + '%d,%d-%d,%d', + rect['start_row'], + rect['start_col'], + rect['end_row'], + rect['end_col'] + ) + + t.eq(expected, actual) +end + +local function wantencoding() + local encoding = t.ffi.new('VTermEncodingInstance') + encoding['enc'] = vterm.vterm_lookup_encoding(vterm.ENC_UTF8, string.byte('u')) + if encoding.enc.init then + encoding.enc.init(encoding.enc, encoding['data']) + end + return encoding +end + +local function encin(input, encoding) + local len = string.len(input) + + local cp = t.ffi.new('uint32_t[?]', len) + local cpi = t.ffi.new('int[1]') + local pos = t.ffi.new('size_t[1]', 0) + + encoding.enc.decode(encoding.enc, encoding.data, cp, cpi, len, input, pos, len) + + local f = assert(io.open(t.paths.vterm_test_file, 'w')) + if tonumber(cpi[0]) > 0 then + f:write('encout ') + for i = 0, cpi[0] - 1 do + if i == 0 then + f:write(string.format('%x', cp[i])) + else + f:write(string.format(',%x', cp[i])) + end + end + f:write('\n') + end + f:close() +end + +local function strpe_modifiers(input_mod) + local mod = t.ffi.new('VTermModifier') ---@type any + if input_mod.C then + mod = bit.bor(mod, vterm.VTERM_MOD_CTRL) + end + if input_mod.S then + mod = bit.bor(mod, vterm.VTERM_MOD_SHIFT) + end + if input_mod.A then + mod = bit.bor(mod, vterm.VTERM_MOD_ALT) + end + return mod +end + +local function strp_key(input_key) + if input_key == 'up' then + return vterm.VTERM_KEY_UP + end + + if input_key == 'tab' then + return vterm.VTERM_KEY_TAB + end + + if input_key == 'enter' then + return vterm.VTERM_KEY_ENTER + end + + if input_key == 'f1' then + return vterm.VTERM_KEY_FUNCTION_0 + 1 + end + + if input_key == 'kp0' then + return vterm.VTERM_KEY_KP_0 + end + + return vterm.VTERM_KEY_NONE +end + +local function mousemove(row, col, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + vterm.vterm_mouse_move(vt, row, col, mod) +end + +local function mousebtn(press, button, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + local flag = press == 'd' or press == 'D' + vterm.vterm_mouse_button(vt, button, flag, mod) +end + +local function inchar(c, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + vterm.vterm_keyboard_unichar(vt, c, mod) +end + +local function inkey(input_key, vt, input_mod) + input_mod = input_mod or {} + local mod = strpe_modifiers(input_mod) + local key = strp_key(input_key) + vterm.vterm_keyboard_key(vt, key, mod) +end + +before_each(function() + vim.fs.rm(t.paths.vterm_test_file, { force = true }) +end) + +describe('vterm', function() + itp('02parser', function() + local vt = init() + vterm.vterm_set_utf8(vt, false) + wantparser(vt) + + -- Basic text + push('hello', vt) + expect('text 68,65,6c,6c,6f') + + -- C0 + push('\x03', vt) + expect('control 03') + push('\x1f', vt) + expect('control 1f') + + -- C1 8bit + push('\x83', vt) + expect('control 83') + push('\x99', vt) + expect('control 99') + + -- C1 7bit + push('\x1b\x43', vt) + expect('control 83') + push('\x1b\x59', vt) + expect('control 99') + + -- High bytes + push('\xa0\xcc\xfe', vt) + expect('text a0,cc,fe') + + -- Mixed + push('1\n2', vt) + expect('text 31\ncontrol 0a\ntext 32') + + -- Escape + push('\x1b=', vt) + expect('escape =') + + -- Escape 2-byte + push('\x1b(X', vt) + expect('escape (X') + + -- Split write Escape + push('\x1b(', vt) + push('Y', vt) + expect('escape (Y') + + -- Escape cancels Escape, starts another + push('\x1b(\x1b)Z', vt) + expect('escape )Z') + + -- CAN cancels Escape, returns to normal mode + push('\x1b(\x18AB', vt) + expect('text 41,42') + + -- C0 in Escape interrupts and continues + push('\x1b(\nX', vt) + expect('control 0a\nescape (X') + + -- CSI 0 args + push('\x1b[a', vt) + expect('csi 61 *') + + -- CSI 1 arg + push('\x1b[9b', vt) + expect('csi 62 9') + + -- CSI 2 args + push('\x1b[3;4c', vt) + expect('csi 63 3,4') + + -- CSI 1 arg 1 sub + push('\x1b[1:2c', vt) + expect('csi 63 1+,2') + + -- CSI many digits + push('\x1b[678d', vt) + expect('csi 64 678') + + -- CSI leading zero + push('\x1b[007e', vt) + expect('csi 65 7') + + -- CSI qmark + push('\x1b[?2;7f', vt) + expect('csi 66 L=3f 2,7') + + -- CSI greater + push('\x1b[>c', vt) + expect('csi 63 L=3e *') + + -- CSI SP + push('\x1b[12 q', vt) + expect('csi 71 12 I=20') + + -- Mixed CSI + push('A\x1b[8mB', vt) + expect('text 41\ncsi 6d 8\ntext 42') + + -- Split write + push('\x1b', vt) + push('[a', vt) + expect('csi 61 *') + push('foo\x1b[', vt) + expect('text 66,6f,6f') + push('4b', vt) + expect('csi 62 4') + push('\x1b[12;', vt) + push('3c', vt) + expect('csi 63 12,3') + + -- Escape cancels CSI, starts Escape + push('\x1b[123\x1b9', vt) + expect('escape 9') + + -- CAN cancels CSI, returns to normal mode + push('\x1b[12\x18AB', vt) + expect('text 41,42') + + -- C0 in Escape interrupts and continues + push('\x1b(\nX', vt) + expect('control 0a\nescape (X') + + -- OSC BEL + push('\x1b]1;Hello\x07', vt) + expect('osc [1;Hello]') + + -- OSC ST (7bit) + push('\x1b]1;Hello\x1b\\', vt) + expect('osc [1;Hello]') + + -- OSC ST (8bit) + push('\x9d1;Hello\x9c', vt) + expect('osc [1;Hello]') + + -- OSC in parts + push('\x1b]52;abc', vt) + expect('osc [52;abc') + push('def', vt) + expect('osc def') + push('ghi\x1b\\', vt) + expect('osc ghi]') + + -- OSC BEL without semicolon + push('\x1b]1234\x07', vt) + expect('osc [1234;]') + + -- OSC ST without semicolon + push('\x1b]1234\x1b\\', vt) + expect('osc [1234;]') + + -- Escape cancels OSC, starts Escape + push('\x1b]Something\x1b9', vt) + expect('escape 9') + + -- CAN cancels OSC, returns to normal mode + push('\x1b]12\x18AB', vt) + expect('text 41,42') + + -- C0 in OSC interrupts and continues + push('\x1b]2;\nBye\x07', vt) + expect('osc [2;\ncontrol 0a\nosc Bye]') + + -- DCS BEL + push('\x1bPHello\x07', vt) + expect('dcs [Hello]') + + -- DCS ST (7bit) + push('\x1bPHello\x1b\\', vt) + expect('dcs [Hello]') + + -- DCS ST (8bit) + push('\x90Hello\x9c', vt) + expect('dcs [Hello]') + + -- Split write of 7bit ST + push('\x1bPABC\x1b', vt) + expect('dcs [ABC') + push('\\', vt) + expect('dcs ]') + + -- Escape cancels DCS, starts Escape + push('\x1bPSomething\x1b9', vt) + expect('escape 9') + + -- CAN cancels DCS, returns to normal mode + push('\x1bP12\x18AB', vt) + expect('text 41,42') + + -- C0 in OSC interrupts and continues + push('\x1bPBy\ne\x07', vt) + expect('dcs [By\ncontrol 0a\ndcs e]') + + -- APC BEL + push('\x1b_Hello\x07', vt) + expect('apc [Hello]') + + -- APC ST (7bit) + push('\x1b_Hello\x1b\\', vt) + expect('apc [Hello]') + + -- APC ST (8bit) + push('\x9fHello\x9c', vt) + expect('apc [Hello]') + + -- PM BEL + push('\x1b^Hello\x07', vt) + expect('pm [Hello]') + + -- PM ST (7bit) + push('\x1b^Hello\x1b\\', vt) + expect('pm [Hello]') + + -- PM ST (8bit) + push('\x9eHello\x9c', vt) + expect('pm [Hello]') + + -- SOS BEL + push('\x1bXHello\x07', vt) + expect('sos [Hello]') + + -- SOS ST (7bit) + push('\x1bXHello\x1b\\', vt) + expect('sos [Hello]') + + -- SOS ST (8bit) + push('\x98Hello\x9c', vt) + expect('sos [Hello]') + + push('\x1bXABC\x01DEF\x1b\\', vt) + expect('sos [ABC\x01DEF]') + push('\x1bXABC\x99DEF\x1b\\', vt) + expect('sos [ABC\x99DEF]') + + -- NUL ignored + push('\x00', vt) + + -- NUL ignored within CSI + push('\x1b[12\x003m', vt) + expect('csi 6d 123') + + -- DEL ignored + push('\x7f', vt) + + -- DEL ignored within CSI + push('\x1b[12\x7f3m', vt) + expect('csi 6d 123') + + -- DEL inside text" + push('AB\x7fC', vt) + expect('text 41,42\ntext 43') + end) + + itp('03encoding_utf8', function() + local encoding = wantencoding() + + -- Low + encin('123', encoding) + expect('encout 31,32,33') + + -- We want to prove the UTF-8 parser correctly handles all the sequences. + -- Easy way to do this is to check it does low/high boundary cases, as that + -- leaves only two for each sequence length + -- + -- These ranges are therefore: + -- + -- Two bytes: + -- U+0080 = 000 10000000 => 00010 000000 + -- => 11000010 10000000 = C2 80 + -- U+07FF = 111 11111111 => 11111 111111 + -- => 11011111 10111111 = DF BF + -- + -- Three bytes: + -- U+0800 = 00001000 00000000 => 0000 100000 000000 + -- => 11100000 10100000 10000000 = E0 A0 80 + -- U+FFFD = 11111111 11111101 => 1111 111111 111101 + -- => 11101111 10111111 10111101 = EF BF BD + -- (We avoid U+FFFE and U+FFFF as they're invalid codepoints) + -- + -- Four bytes: + -- U+10000 = 00001 00000000 00000000 => 000 010000 000000 000000 + -- => 11110000 10010000 10000000 10000000 = F0 90 80 80 + -- U+1FFFFF = 11111 11111111 11111111 => 111 111111 111111 111111 + -- => 11110111 10111111 10111111 10111111 = F7 BF BF BF + + -- 2 byte + encin('\xC2\x80\xDF\xBF', encoding) + expect('encout 80,7ff') + + -- 3 byte + encin('\xE0\xA0\x80\xEF\xBF\xBD', encoding) + expect('encout 800,fffd') + + -- 4 byte + encin('\xF0\x90\x80\x80\xF7\xBF\xBF\xBF', encoding) + expect('encout 10000,1fffff') + + -- Next up, we check some invalid sequences + -- + Early termination (back to low bytes too soon) + -- + Early restart (another sequence introduction before the previous one was finished) + + -- Early termination + encin('\xC2!', encoding) + expect('encout fffd,21') + + encin('\xE0!\xE0\xA0!', encoding) + expect('encout fffd,21,fffd,21') + + encin('\xF0!\xF0\x90!\xF0\x90\x80!', encoding) + expect('encout fffd,21,fffd,21,fffd,21') + + -- Early restart + encin('\xC2\xC2\x90', encoding) + expect('encout fffd,90') + + encin('\xE0\xC2\x90\xE0\xA0\xC2\x90', encoding) + expect('encout fffd,90,fffd,90') + + encin('\xF0\xC2\x90\xF0\x90\xC2\x90\xF0\x90\x80\xC2\x90', encoding) + expect('encout fffd,90,fffd,90,fffd,90') + + -- Test the overlong sequences by giving an overlong encoding of U+0000 and + -- an encoding of the highest codepoint still too short + -- + -- Two bytes: + -- U+0000 = C0 80 + -- U+007F = 000 01111111 => 00001 111111 => + -- => 11000001 10111111 => C1 BF + -- + -- Three bytes: + -- U+0000 = E0 80 80 + -- U+07FF = 00000111 11111111 => 0000 011111 111111 + -- => 11100000 10011111 10111111 = E0 9F BF + -- + -- Four bytes: + -- U+0000 = F0 80 80 80 + -- U+FFFF = 11111111 11111111 => 000 001111 111111 111111 + -- => 11110000 10001111 10111111 10111111 = F0 8F BF BF + + -- Overlong + encin('\xC0\x80\xC1\xBF', encoding) + expect('encout fffd,fffd') + + encin('\xE0\x80\x80\xE0\x9F\xBF', encoding) + expect('encout fffd,fffd') + + encin('\xF0\x80\x80\x80\xF0\x8F\xBF\xBF', encoding) + expect('encout fffd,fffd') + + -- UTF-16 surrogates U+D800 and U+DFFF + -- UTF-16 Surrogates + encin('\xED\xA0\x80\xED\xBF\xBF', encoding) + expect('encout fffd,fffd') + + -- Split write + encin('\xC2', encoding) + encin('\xA0', encoding) + expect('encout a0') + + encin('\xE0', encoding) + encin('\xA0\x80', encoding) + expect('encout 800') + encin('\xE0\xA0', encoding) + encin('\x80', encoding) + expect('encout 800') + + encin('\xF0', encoding) + encin('\x90\x80\x80', encoding) + expect('encout 10000') + encin('\xF0\x90', encoding) + encin('\x80\x80', encoding) + expect('encout 10000') + encin('\xF0\x90\x80', encoding) + encin('\x80', encoding) + expect('encout 10000') + end) + + itp('10state_putglyph', function() + local vt = init() + local state = wantstate(vt, { g = true }) + + -- Low + reset(state, nil) + push('ABC', vt) + expect('putglyph 41 1 0,0\nputglyph 42 1 0,1\nputglyph 43 1 0,2') + + -- UTF-8 1 char + -- U+00C1 = 0xC3 0x81 name: LATIN CAPITAL LETTER A WITH ACUTE + -- U+00E9 = 0xC3 0xA9 name: LATIN SMALL LETTER E WITH ACUTE + reset(state, nil) + push('\xC3\x81\xC3\xA9', vt) + expect('putglyph c1 1 0,0\nputglyph e9 1 0,1') + + -- UTF-8 split writes + reset(state, nil) + push('\xC3', vt) + push('\x81', vt) + expect('putglyph c1 1 0,0') + + -- UTF-8 wide char + -- U+FF10 = EF BC 90 name: FULLWIDTH DIGIT ZERO + reset(state, nil) + push('\xEF\xBC\x90 ', vt) + expect('putglyph ff10 2 0,0\nputglyph 20 1 0,2') + + -- UTF-8 emoji wide char + -- U+1F600 = F0 9F 98 80 name: GRINNING FACE + reset(state, nil) + push('\xF0\x9F\x98\x80 ', vt) + expect('putglyph 1f600 2 0,0\nputglyph 20 1 0,2') + + -- UTF-8 combining chars + -- U+0301 = CC 81 name: COMBINING ACUTE + reset(state, nil) + push('e\xCC\x81Z', vt) + expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1') + + -- Combining across buffers + reset(state, nil) + push('e', vt) + expect('putglyph 65 1 0,0') + push('\xCC\x81Z', vt) + expect('putglyph 65,301 1 0,0\nputglyph 5a 1 0,1') + + -- Spare combining chars get truncated + reset(state, nil) + push('e' .. string.rep('\xCC\x81', 10), vt) + expect('putglyph 65,301,301,301,301,301 1 0,0') -- and nothing more + + reset(state, nil) + push('e', vt) + expect('putglyph 65 1 0,0') + push('\xCC\x81', vt) + expect('putglyph 65,301 1 0,0') + push('\xCC\x82', vt) + expect('putglyph 65,301,302 1 0,0') + + -- DECSCA protected + reset(state, nil) + push('A\x1b[1"qB\x1b[2"qC', vt) + expect('putglyph 41 1 0,0\nputglyph 42 1 0,1 prot\nputglyph 43 1 0,2') + end) + + itp('11state_movecursor', function() + local vt = init() + local state = wantstate(vt) + + -- Implicit + push('ABC', vt) + cursor(0, 3, state) + + -- Backspace + push('\b', vt) + cursor(0, 2, state) + -- Horizontal Tab + push('\t', vt) + cursor(0, 8, state) + -- Carriage Return + push('\r', vt) + cursor(0, 0, state) + -- Linefeed + push('\n', vt) + cursor(1, 0, state) + + -- Backspace bounded by lefthand edge + push('\x1b[4;2H', vt) + cursor(3, 1, state) + push('\b', vt) + cursor(3, 0, state) + push('\b', vt) + cursor(3, 0, state) + + -- Backspace cancels phantom + push('\x1b[4;80H', vt) + cursor(3, 79, state) + push('X', vt) + cursor(3, 79, state) + push('\b', vt) + cursor(3, 78, state) + + -- HT bounded by righthand edge + push('\x1b[1;78H', vt) + cursor(0, 77, state) + push('\t', vt) + cursor(0, 79, state) + push('\t', vt) + cursor(0, 79, state) + + reset(state, nil) + + -- Index + push('ABC\x1bD', vt) + cursor(1, 3, state) + -- Reverse Index + push('\x1bM', vt) + cursor(0, 3, state) + -- Newline + push('\x1bE', vt) + cursor(1, 0, state) + + reset(state, nil) + + -- Cursor Forward + push('\x1b[B', vt) + cursor(1, 0, state) + push('\x1b[3B', vt) + cursor(4, 0, state) + push('\x1b[0B', vt) + cursor(5, 0, state) + + -- Cursor Down + push('\x1b[C', vt) + cursor(5, 1, state) + push('\x1b[3C', vt) + cursor(5, 4, state) + push('\x1b[0C', vt) + cursor(5, 5, state) + + -- Cursor Up + push('\x1b[A', vt) + cursor(4, 5, state) + push('\x1b[3A', vt) + cursor(1, 5, state) + push('\x1b[0A', vt) + cursor(0, 5, state) + + -- Cursor Backward + push('\x1b[D', vt) + cursor(0, 4, state) + push('\x1b[3D', vt) + cursor(0, 1, state) + push('\x1b[0D', vt) + cursor(0, 0, state) + + -- Cursor Next Line + push(' ', vt) + cursor(0, 3, state) + push('\x1b[E', vt) + cursor(1, 0, state) + push(' ', vt) + cursor(1, 3, state) + push('\x1b[2E', vt) + cursor(3, 0, state) + push('\x1b[0E', vt) + cursor(4, 0, state) + + -- Cursor Previous Line + push(' ', vt) + cursor(4, 3, state) + push('\x1b[F', vt) + cursor(3, 0, state) + push(' ', vt) |