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 | |
| 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.
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | cmake.config/config.h.in | 2 | ||||
| -rw-r--r-- | src/vterm/vterm.c | 509 | ||||
| -rw-r--r-- | src/vterm/vterm.h | 37 | ||||
| -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 |
7 files changed, 4146 insertions, 2 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index f6c5427c7d..764d670973 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ set(DEPS_IGNORE_SHA FALSE) #------------------------------------------------------------------------------- set(FUNCS_DATA ${PROJECT_BINARY_DIR}/funcs_data.mpack) set(TOUCHES_DIR ${PROJECT_BINARY_DIR}/touches) +set(VTERM_TEST_FILE ${PROJECT_BINARY_DIR}/test/vterm_test_output) file(GLOB DOCFILES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/runtime/doc/*.txt) diff --git a/cmake.config/config.h.in b/cmake.config/config.h.in index fd712032c3..833218abf9 100644 --- a/cmake.config/config.h.in +++ b/cmake.config/config.h.in @@ -52,3 +52,5 @@ #cmakedefine HAVE_BUILTIN_ADD_OVERFLOW #cmakedefine HAVE_WIMPLICIT_FALLTHROUGH_FLAG #cmakedefine HAVE_BITSCANFORWARD64 + +#define VTERM_TEST_FILE "@VTERM_TEST_FILE@" diff --git a/src/vterm/vterm.c b/src/vterm/vterm.c index 870a61566e..e8c87929e2 100644 --- a/src/vterm/vterm.c +++ b/src/vterm/vterm.c @@ -1,8 +1,9 @@ #include "vterm_internal.h" +#include "auto/config.h" +#include <stdarg.h> #include <stdio.h> #include <stdlib.h> -#include <stdarg.h> #include <string.h> /***************** @@ -429,3 +430,509 @@ void vterm_check_version(int major, int minor) // Happy } + +// For unit tests. +#ifndef NDEBUG + +int parser_text(const char bytes[], size_t len, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "text "); + int i; + for(i = 0; i < len; i++) { + unsigned char b = bytes[i]; + if(b < 0x20 || b == 0x7f || (b >= 0x80 && b < 0xa0)) { + break; + } + fprintf(f, i ? ",%x" : "%x", b); + } + fprintf(f, "\n"); + fclose(f); + + return i; +} + +static void printchars(const char *s, size_t len, FILE *f) +{ + while(len--) { + fprintf(f, "%c", (s++)[0]); + } +} + +int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "csi %02x", command); + + if(leader && leader[0]) { + fprintf(f, " L="); + for(int i = 0; leader[i]; i++) { + fprintf(f, "%02x", leader[i]); + } + } + + for(int i = 0; i < argcount; i++) { + char sep = i ? ',' : ' '; + + if(args[i] == CSI_ARG_MISSING) { + fprintf(f, "%c*", sep); + } else { + fprintf(f, "%c%ld%s", sep, CSI_ARG(args[i]), CSI_ARG_HAS_MORE(args[i]) ? "+" : ""); + } + } + + if(intermed && intermed[0]) { + fprintf(f, " I="); + for(int i = 0; intermed[i]; i++) { + fprintf(f, "%02x", intermed[i]); + } + } + + fprintf(f, "\n"); + + fclose(f); + + return 1; +} + +int parser_osc(int command, VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "osc "); + + if(frag.initial) { + if(command == -1) { + fprintf(f, "["); + } else { + fprintf(f, "[%d;", command); + } + } + + printchars(frag.str, frag.len, f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "dcs "); + + if(frag.initial) { + fprintf(f, "["); + for(int i = 0; i < commandlen; i++) { + fprintf(f, "%c", command[i]); + } + } + + printchars(frag.str, frag.len,f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_apc(VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "apc "); + + if(frag.initial) { + fprintf(f, "["); + } + + printchars(frag.str, frag.len, f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_pm(VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "pm "); + + if(frag.initial) { + fprintf(f, "["); + } + + printchars(frag.str, frag.len,f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int parser_sos(VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "sos "); + + if(frag.initial) { + fprintf(f, "["); + } + + printchars(frag.str, frag.len,f); + + if(frag.final) { + fprintf(f, "]"); + } + + fprintf(f, "\n"); + fclose(f); + + return 1; +} + +int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "selection-set mask=%04X ", mask); + if(frag.initial) { + fprintf(f, "["); +} + printchars(frag.str, frag.len, f); + if(frag.final) { + fprintf(f, "]"); +} + fprintf(f,"\n"); + + fclose(f); + return 1; +} + +int selection_query(VTermSelectionMask mask, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"selection-query mask=%04X\n", mask); + + fclose(f); + return 1; +} + +bool want_state_putglyph; +int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user) +{ + if(!want_state_putglyph) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "putglyph "); + for(int i = 0; i < VTERM_MAX_CHARS_PER_CELL && info->chars[i]; i++) { + fprintf(f, i ? ",%x" : "%x", info->chars[i]); + } + fprintf(f, " %d %d,%d", info->width, pos.row, pos.col); + if(info->protected_cell) { + fprintf(f, " prot"); + } + if(info->dwl) { + fprintf(f, " dwl"); + } + if(info->dhl) { + fprintf(f, " dhl-%s", info->dhl == 1 ? "top" : info->dhl == 2 ? "bottom" : "?" ); + } + fprintf(f, "\n"); + + fclose(f); + + return 1; +} + +bool want_state_movecursor; +VTermPos state_pos; +int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + state_pos = pos; + + if(want_state_movecursor) { + fprintf(f,"movecursor %d,%d\n", pos.row, pos.col); + } + + fclose(f); + return 1; +} + +bool want_state_scrollrect; +int state_scrollrect(VTermRect rect, int downward, int rightward, void *user) +{ + if(!want_state_scrollrect) { + return 0; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + + fprintf(f,"scrollrect %d..%d,%d..%d => %+d,%+d\n", + rect.start_row, rect.end_row, rect.start_col, rect.end_col, + downward, rightward); + + fclose(f); + return 1; +} + +bool want_state_moverect; +int state_moverect(VTermRect dest, VTermRect src, void *user) +{ + if(!want_state_moverect) { + return 0; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"moverect %d..%d,%d..%d -> %d..%d,%d..%d\n", + src.start_row, src.end_row, src.start_col, src.end_col, + dest.start_row, dest.end_row, dest.start_col, dest.end_col); + + fclose(f); + return 1; +} + +void print_color(const VTermColor *col) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + if (VTERM_COLOR_IS_RGB(col)) { + fprintf(f,"rgb(%d,%d,%d", col->rgb.red, col->rgb.green, col->rgb.blue); + } + else if (VTERM_COLOR_IS_INDEXED(col)) { + fprintf(f,"idx(%d", col->indexed.idx); + } + else { + fprintf(f,"invalid(%d", col->type); + } + if (VTERM_COLOR_IS_DEFAULT_FG(col)) { + fprintf(f,",is_default_fg"); + } + if (VTERM_COLOR_IS_DEFAULT_BG(col)) { + fprintf(f,",is_default_bg"); + } + fprintf(f,")"); + fclose(f); +} + +bool want_state_settermprop; +int state_settermprop(VTermProp prop, VTermValue *val, void *user) +{ + if(!want_state_settermprop) { + return 1; + } + + int errcode = 0; + FILE *f = fopen(VTERM_TEST_FILE, "a"); + + VTermValueType type = vterm_get_prop_type(prop); + switch(type) { + case VTERM_VALUETYPE_BOOL: + fprintf(f,"settermprop %d %s\n", prop, val->boolean ? "true" : "false"); + errcode = 1; + goto end; + case VTERM_VALUETYPE_INT: + fprintf(f,"settermprop %d %d\n", prop, val->number); + errcode = 1; + goto end; + case VTERM_VALUETYPE_STRING: + fprintf(f,"settermprop %d %s\"%.*s\"%s\n", prop, + val->string.initial ? "[" : "", (int)val->string.len, val->string.str, val->string.final ? "]" : ""); + errcode=0; + goto end; + case VTERM_VALUETYPE_COLOR: + fprintf(f,"settermprop %d ", prop); + print_color(&val->color); + fprintf(f,"\n"); + errcode=1; + goto end; + case VTERM_N_VALUETYPES: + goto end; + } + +end: + fclose(f); + return errcode; +} + +bool want_state_erase; +int state_erase(VTermRect rect, int selective, void *user) +{ + if(!want_state_erase) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + + fprintf(f,"erase %d..%d,%d..%d%s\n", + rect.start_row, rect.end_row, rect.start_col, rect.end_col, + selective ? " selective" : ""); + + fclose(f); + return 1; +} + +struct { + int bold; + int underline; + int italic; + int blink; + int reverse; + int conceal; + int strike; + int font; + int small; + int baseline; + VTermColor foreground; + VTermColor background; +} state_pen; + +int state_setpenattr(VTermAttr attr, VTermValue *val, void *user) +{ + switch(attr) { + case VTERM_ATTR_BOLD: + state_pen.bold = val->boolean; + break; + case VTERM_ATTR_UNDERLINE: + state_pen.underline = val->number; + break; + case VTERM_ATTR_ITALIC: + state_pen.italic = val->boolean; + break; + case VTERM_ATTR_BLINK: + state_pen.blink = val->boolean; + break; + case VTERM_ATTR_REVERSE: + state_pen.reverse = val->boolean; + break; + case VTERM_ATTR_CONCEAL: + state_pen.conceal = val->boolean; + break; + case VTERM_ATTR_STRIKE: + state_pen.strike = val->boolean; + break; + case VTERM_ATTR_FONT: + state_pen.font = val->number; + break; + case VTERM_ATTR_SMALL: + state_pen.small = val->boolean; + break; + case VTERM_ATTR_BASELINE: + state_pen.baseline = val->number; + break; + case VTERM_ATTR_FOREGROUND: + state_pen.foreground = val->color; + break; + case VTERM_ATTR_BACKGROUND: + state_pen.background = val->color; + break; + + case VTERM_N_ATTRS: + return 0; + default: + break; + } + + return 1; +} + +bool want_state_scrollback; +int state_sb_clear(void *user) { + if(!want_state_scrollback) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"sb_clear\n"); + fclose(f); + + return 0; +} + +bool want_screen_scrollback; +int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user) +{ + if(!want_screen_scrollback) { + return 1; + } + + int eol = cols; + while(eol && !cells[eol-1].chars[0]) { + eol--; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "sb_pushline %d =", cols); + for(int c = 0; c < eol; c++) { + fprintf(f, " %02X", cells[c].chars[0]); + } + fprintf(f, "\n"); + + fclose(f); + + return 1; +} + +int screen_sb_popline(int cols, VTermScreenCell *cells, void *user) +{ + if(!want_screen_scrollback) { + return 0; + } + + // All lines of scrollback contain "ABCDE" + for(int col = 0; col < cols; col++) { + if(col < 5) { + cells[col].chars[0] = 'A' + col; + } else { + cells[col].chars[0] = 0; + } + + cells[col].width = 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f,"sb_popline %d\n", cols); + fclose(f); + return 1; +} + +int screen_sb_clear(void *user) +{ + if(!want_screen_scrollback) { + return 1; + } + + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "sb_clear\n"); + fclose(f); + return 0; +} + +void term_output(const char *s, size_t len, void *user) +{ + FILE *f = fopen(VTERM_TEST_FILE, "a"); + fprintf(f, "output "); + for(int i = 0; i < len; i++) { + fprintf(f, "%x%s", (unsigned char)s[i], i < len-1 ? "," : "\n"); + } + fclose(f); +} + +#endif diff --git a/src/vterm/vterm.h b/src/vterm/vterm.h index 929418c63a..df6878f744 100644 --- a/src/vterm/vterm.h +++ b/src/vterm/vterm.h @@ -9,6 +9,7 @@ extern "C" { #include <stdlib.h> #include <stdbool.h> +#include "nvim/macros_defs.h" #include "vterm_keycodes.h" #define VTERM_VERSION_MAJOR 0 @@ -21,7 +22,7 @@ extern "C" { /* Any cell can contain at most one basic printing character and 5 combining * characters. This number could be changed but will be ABI-incompatible if * you do */ -#define VTERM_MAX_CHARS_PER_CELL 6 +enum{ VTERM_MAX_CHARS_PER_CELL=6}; typedef struct VTerm VTerm; typedef struct VTermState VTermState; @@ -634,6 +635,40 @@ void vterm_copy_cells(VTermRect dest, void (*copycell)(VTermPos dest, VTermPos src, void *user), void *user); +#ifndef NDEBUG +int parser_text(const char bytes[], size_t len, void *user); +int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user); +int parser_osc(int command, VTermStringFragment frag, void *user); +int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user); +int parser_apc(VTermStringFragment frag, void *user); +int parser_pm(VTermStringFragment frag, void *user); +int parser_sos(VTermStringFragment frag, void *user); +int selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user); +int selection_query(VTermSelectionMask mask, void *user); +int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user); +int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user); +int state_scrollrect(VTermRect rect, int downward, int rightward, void *user); +int state_moverect(VTermRect dest, VTermRect src, void *user); +int state_settermprop(VTermProp prop, VTermValue *val, void *user); +int state_erase(VTermRect rect, int selective, void *user); +int state_setpenattr(VTermAttr attr, VTermValue *val, void *user); +int state_sb_clear(void *user); +void print_color(const VTermColor *col); +int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user); +int screen_sb_popline(int cols, VTermScreenCell *cells, void *user); +int screen_sb_clear(void *user); +void term_output(const char *s, size_t len, void *user); +EXTERN VTermPos state_pos; +EXTERN bool want_state_putglyph INIT (=false); +EXTERN bool want_state_movecursor INIT(= false); +EXTERN bool want_state_erase INIT(= false); +EXTERN bool want_state_scrollrect INIT(= false); +EXTERN bool want_state_moverect INIT(= false); +EXTERN bool want_state_settermprop INIT(= false); +EXTERN bool want_state_scrollback INIT(= false); +EXTERN bool want_screen_scrollback INIT(= false); +#endif + #ifdef __cplusplus } #endif 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:writ |