URI: 
       tAdd basic infrastructure for sort of automated tests - ledit - Text editor (WIP)
  HTML git clone git://lumidify.org/ledit.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 225b064c19ce96a38c5472b3d73367922f671dc7
   DIR parent 155a081f559a9ecfddd8b086c7d91187c2af4bc2
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Sat,  4 Nov 2023 19:11:19 +0100
       
       Add basic infrastructure for sort of automated tests
       
       Now that I have the infrastructure, I can forget about
       writing the actual tests. That's how it works, right?
       
       Diffstat:
         M Makefile                            |       3 ++-
         M keys_basic.c                        |       9 +--------
         M keys_basic.h                        |       2 +-
         M keys_command.c                      |      38 ++++++++++++++++++++++---------
         M keys_command.h                      |       2 +-
         M ledit.c                             |     365 ++++++++++++++++++++++++++++++-
         A tests/README                        |      59 +++++++++++++++++++++++++++++++
         M undo.c                              |      27 +++++++++++++++------------
         M undo.h                              |       4 ++++
         M view.h                              |       2 +-
       
       10 files changed, 466 insertions(+), 45 deletions(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       t@@ -12,6 +12,7 @@ MAN5 = leditrc.5
        MISCFILES = Makefile README LICENSE IDEAS NOTES TODO
        
        DEBUG=0
       +TEST=0
        SANITIZE=0
        ENABLE_UTF8PROC=1
        
       t@@ -78,7 +79,7 @@ EXTRA_LDFLAGS_UTF8PROC1 = `pkg-config --libs libutf8proc`
        # Xcursor isn't actually needed right now since I'm not using the drag 'n drop functionality
        # of ctrlsel yet, but since it's moderately likely that I will use that in the future, I
        # decided to just leave it in.
       -CFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} ${EXTRA_CFLAGS_UTF8PROC${ENABLE_UTF8PROC}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L -std=c99 `pkg-config --cflags x11 xkbfile pangoxft xext xcursor`
       +CFLAGS_LEDIT = -DTEST=${TEST} ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_CFLAGS_DEBUG${DEBUG}} ${EXTRA_CFLAGS_UTF8PROC${ENABLE_UTF8PROC}} -Wall -Wextra -pedantic -D_POSIX_C_SOURCE=200809L -std=c99 `pkg-config --cflags x11 xkbfile pangoxft xext xcursor`
        LDFLAGS_LEDIT = ${EXTRA_FLAGS_SANITIZE${SANITIZE}} ${EXTRA_LDFLAGS_DEBUG${DEBUG}} ${EXTRA_LDFLAGS_UTF8PROC${ENABLE_UTF8PROC}} `pkg-config --libs x11 xkbfile pangoxft xext xcursor` -lm
        
        all: ${BIN}
   DIR diff --git a/keys_basic.c b/keys_basic.c
       t@@ -2709,14 +2709,7 @@ repeat_command(ledit_view *view, char *text, size_t len) {
        }
        
        struct action
       -basic_key_handler(ledit_view *view, XEvent *event, int lang_index) {
       -        char *buf = NULL;
       -        KeySym sym = NoSymbol;
       -        int n;
       -
       -        unsigned int key_state = event->xkey.state;
       -        preprocess_key(view->window, &event->xkey, &sym, &buf, &n);
       -
       +basic_key_handler(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int n, int lang_index) {
                struct repetition_stack_elem *re = push_repetition_stack();
                re->key_text = ledit_strndup(buf, (size_t)n);
                re->len = (size_t)n;
   DIR diff --git a/keys_basic.h b/keys_basic.h
       t@@ -11,6 +11,6 @@ int basic_key_cb_modemask_is_valid(basic_key_cb *cb, ledit_mode modes);
        
        /* perform cleanup of global data */
        void basic_key_cleanup(void);
       -struct action basic_key_handler(ledit_view *view, XEvent *event, int lang_index);
       +struct action basic_key_handler(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int n, int lang_index);
        
        #endif
   DIR diff --git a/keys_command.c b/keys_command.c
       t@@ -215,11 +215,16 @@ static int parse_range(
        static int handle_cmd(ledit_view *view, char *cmd, size_t len, size_t lang_index);
        
        /* FIXME: USE LEN EVERYWHERE INSTEAD OF RELYING ON cmd BEING NUL-TERMINATED */
       -/* FIXME: return error so write_quit knows when to quit */
       +/* returns 1 on error, 0 otherwise */
        static int
       -handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2) {
       -        (void)l1;
       -        (void)l2;
       +handle_write_base(ledit_view *view, char *cmd) {
       +        #if TEST
       +        /* disallow normal file writing in test mode so no
       +           file can accidentally be destroyed by fuzz testing */
       +        (void)view;
       +        (void)cmd;
       +        return 0;
       +        #else
                /* FIXME: allow writing only part of file */
                char *filename = view->buffer->filename;
                int stored = 1;
       t@@ -248,6 +253,7 @@ handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2) {
                                    "%s: file modification time changed; use ! to override",
                                    filename
                                );
       +                        return 1;
                        /* FIXME: I guess the file can still exist if stat returns an error,
                           but the writing itself will probably fail then as well. */
                        } else if (!ret && !force && !stored) {
       t@@ -256,8 +262,10 @@ handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2) {
                                    "%s: file exists; use ! to override",
                                    filename
                                );
       +                        return 1;
                        } else if (buffer_write_to_filename(view->buffer, filename, &errstr)) {
                                window_show_message_fmt(view->window, "Error writing %s: %s", filename, errstr);
       +                        return 1;
                        } else {
                                /* FIXME: better message */
                                window_show_message_fmt(view->window, "Wrote file %s", filename);
       t@@ -270,8 +278,18 @@ handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2) {
                        }
                } else {
                        window_show_message(view->window, "No file name", -1);
       +                return 1;
                }
                return 0;
       +        #endif
       +}
       +
       +static int
       +handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2) {
       +        (void)l1;
       +        (void)l2;
       +        handle_write_base(view, cmd);
       +        return 0;
        }
        
        static int
       t@@ -320,7 +338,10 @@ close_view(ledit_view *view, char *cmd, size_t l1, size_t l2) {
        
        static int
        handle_write_quit(ledit_view *view, char *cmd, size_t l1, size_t l2) {
       -        handle_write(view, cmd, l1, l2);
       +        (void)l1;
       +        (void)l2;
       +        if (handle_write_base(view, cmd))
       +                return 0;
                ledit_cleanup();
                exit(0);
                return 0;
       t@@ -984,14 +1005,9 @@ edit_discard(ledit_view *view, char *key_text, size_t len, size_t lang_index) {
        }
        
        struct action
       -command_key_handler(ledit_view *view, XEvent *event, int lang_index) {
       -        char *buf = NULL;
       -        KeySym sym = NoSymbol;
       -        int n;
       +command_key_handler(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int n, int lang_index) {
                command_key_array *cur_keys = config_get_command_keys(lang_index);
                size_t num_keys = cur_keys->num_keys;
       -        unsigned int key_state = event->xkey.state;
       -        preprocess_key(view->window, &event->xkey, &sym, &buf, &n);
                int grabkey = 1, found = 0;
                command_key_cb_flags flags = KEY_FLAG_NONE;
                for (size_t i = 0; i < num_keys; i++) {
   DIR diff --git a/keys_command.h b/keys_command.h
       t@@ -17,6 +17,6 @@ void search_next(ledit_view *view);
        void search_prev(ledit_view *view);
        
        void command_key_cleanup(void);
       -struct action command_key_handler(ledit_view *view, XEvent *event, int lang_index);
       +struct action command_key_handler(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int n, int lang_index);
        
        #endif
   DIR diff --git a/ledit.c b/ledit.c
       t@@ -12,6 +12,9 @@
        
        #include <pwd.h>
        #include <time.h>
       +#if TEST
       +#include <fcntl.h>
       +#endif
        #include <errno.h>
        #include <stdio.h>
        #include <stdlib.h>
       t@@ -45,15 +48,322 @@ static void setup(int argc, char *argv[]);
        static void redraw(void);
        
        static void change_keyboard(char *lang);
       -static void key_press(ledit_view *view, XEvent *event);
       +static void key_press_event(ledit_view *view, XEvent *event);
       +static void key_press(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int n);
        
        ledit_common common;
        ledit_clipboard *clipboard = NULL;
        ledit_buffer *buffer = NULL;
        size_t cur_lang = 0;
        
       +#if TEST
       +static struct {
       +        char *read;                /* text read from stdin */
       +        size_t read_len;              /* length of text in read buffer */
       +        size_t read_alloc;            /* size of read buffer */
       +        size_t line_start;            /* start of current line */
       +        size_t read_cur;              /* length of text already read */
       +} test_status = {NULL, 0, 0, 0, 0};
       +
       +#define READ_BLK_SIZE 128
       +
       +/* Read up to READ_BLK_SIZE bytes from stdin.
       +   Returns 1 if an error occurred, -1 if not new data available, 0 otherwise. */
       +static int
       +read_input(void) {
       +        if (test_status.read_cur > 0) {
       +                memmove(test_status.read, test_status.read + test_status.read_cur, test_status.read_len - test_status.read_cur);
       +                test_status.read_len -= test_status.read_cur;
       +                test_status.read_cur = 0;
       +        }
       +        int nread;
       +        test_status.read_alloc = ideal_array_size(test_status.read_alloc, test_status.read_len + READ_BLK_SIZE);
       +        test_status.read = ledit_realloc(test_status.read, test_status.read_alloc);
       +        nread = read(fileno(stdin), test_status.read + test_status.read_len, READ_BLK_SIZE);
       +        if (nread == -1 && errno == EAGAIN)
       +                return -1;
       +        else if (nread == -1 || nread == 0)
       +                return 1;
       +        test_status.read_len += nread;
       +
       +        return 0;
       +}
       +
       +/* based partially on OpenBSD's strtonum */
       +int
       +read_rangeint(long long *ret, int end, long long min, long long max) {
       +        if (test_status.read_cur >= test_status.read_len || test_status.read[test_status.read_cur] != ' ')
       +                return 1;
       +        char end_char = end ? '\n' : ' ';
       +        size_t len = 0;
       +        test_status.read_cur++;
       +        char *str = test_status.read + test_status.read_cur;
       +        int found = 0;
       +        for (; test_status.read_cur < test_status.read_len; test_status.read_cur++) {
       +                if (test_status.read[test_status.read_cur] == end_char) {
       +                        found = 1;
       +                        break;
       +                }
       +                len++;
       +        }
       +        if (!found || len == 0)
       +                return 1;
       +        /* the string needs to be nul-terminated
       +           if it contains more than 11 characters (10 digits + sign),
       +           it's illegal anyways (at least for these testing purposes...) */
       +        if (len > 11)
       +                return 1;
       +        char nstr[12];
       +        strncpy(nstr, str, len);
       +        nstr[len] = '\0';
       +        char *num_end;
       +        long long ll = strtoll(nstr, &num_end, 10);
       +        if (nstr == num_end || *num_end != '\0' ||
       +            ll < min || ll > max || ((ll == LLONG_MIN ||
       +            ll == LLONG_MAX) && errno == ERANGE)) {
       +                return 1;
       +        }
       +        *ret = ll;
       +        if (end)
       +                test_status.read_cur++;
       +        return 0;
       +}
       +
       +int
       +read_uint(unsigned int *ret, int end) {
       +        long long l;
       +        int err = read_rangeint(&l, end, 0, UINT_MAX);
       +        *ret = (unsigned int)l;
       +        return err;
       +}
       +
       +int
       +read_int(int *ret, int end) {
       +        long long l;
       +        int err = read_rangeint(&l, end, INT_MIN, INT_MAX);
       +        *ret = (int)l;
       +        return err;
       +}
       +
       +int
       +read_text(char **text, size_t *text_len) {
       +        if (test_status.read_cur >= test_status.read_len || test_status.read[test_status.read_cur] != ' ')
       +                return 1;
       +        int bs = 0;
       +        int offset = 0;
       +        test_status.read_cur++;
       +        size_t start = test_status.read_cur;
       +        *text = test_status.read + test_status.read_cur;
       +        int found = 0;
       +        for (; test_status.read_cur < test_status.read_len; test_status.read_cur++) {
       +                if (test_status.read[test_status.read_cur] == '\\') {
       +                        bs++;
       +                        if (bs / 2)
       +                                offset++;
       +                        bs %= 2;
       +                        test_status.read[test_status.read_cur - offset] = '\\';
       +                } else if (test_status.read[test_status.read_cur] == '\n') {
       +                        if (!bs) {
       +                                found = 1;
       +                                break;
       +                        } else {
       +                                bs = 0;
       +                                offset++;
       +                                test_status.read[test_status.read_cur - offset] = '\n';
       +                        }
       +                } else {
       +                        test_status.read[test_status.read_cur - offset] = test_status.read[test_status.read_cur];
       +                        bs = 0;
       +                }
       +        }
       +        if (!found)
       +                return 1;
       +        *text_len = test_status.read_cur - start - offset;
       +        test_status.read_cur++;
       +        return 0;
       +}
       +
       +int
       +read_filename(char **text, size_t *text_len) {
       +        if (read_text(text, text_len))
       +                return 1;
       +        for (size_t i = 0; i < *text_len; i++) {
       +                if ((*text)[i] == '/' || (*text)[i] == '\0')
       +                        return 1;
       +        }
       +        return 0;
       +}
       +
       +static unsigned int view_num = 0;
       +/* Process commands in test_status.
       +   Returns 0 if no complete commands are contained in read buffer, 1 otherwise. */
       +static int
       +process_commands(void) {
       +        int bs = 0;
       +        int found = 0;
       +        size_t nl_index = 0;
       +        for (size_t i = test_status.read_cur; i < test_status.read_len; i++) {
       +                if (test_status.read[i] == '\\') {
       +                        bs++;
       +                        bs %= 2;
       +                } else if (test_status.read[i] == '\n' && bs == 0) {
       +                        found = 1;
       +                        nl_index = i;
       +                        break;
       +                } else {
       +                        bs = 0;
       +                }
       +        }
       +        if (!found)
       +                return 0;
       +        unsigned int key_state, button_num, keysym, new_view;
       +        char *text, *term, *errstr;
       +        size_t text_len;
       +        int x, y;
       +        XEvent e;
       +        FILE *file;
       +        test_status.read_cur += 1;
       +        ledit_view *view = buffer->views[view_num];
       +        switch (test_status.read[test_status.read_cur-1]) {
       +                case 'k':
       +                        /* key press */
       +                        /* k key_state keysym text */
       +                        if (read_uint(&key_state, 0))
       +                                goto error;
       +                        if (read_uint(&keysym, 0))
       +                                goto error;
       +                        if (read_text(&text, &text_len))
       +                                goto error;
       +                        key_press(view, key_state, keysym, text, (int)text_len);
       +                        break;
       +                case 'p':
       +                        /* mouse button press */
       +                        /* p button_num x y */
       +                        if (read_uint(&button_num, 0))
       +                                goto error;
       +                        if (read_int(&x, 0))
       +                                goto error;
       +                        if (read_int(&y, 1))
       +                                goto error;
       +                        e = (XEvent){.xbutton = {.type = ButtonPress, .button = button_num, .x = x, .y = y}};
       +                        window_register_button_press(view->window, &e);
       +                        break;
       +                case 'r':
       +                        /* mouse button release */
       +                        /* r button_num x y */
       +                        if (read_uint(&button_num, 0))
       +                                goto error;
       +                        if (read_int(&x, 0))
       +                                goto error;
       +                        if (read_int(&y, 1))
       +                                goto error;
       +                        e = (XEvent){.xbutton = {.type = ButtonRelease, .button = button_num, .x = x, .y = y}};
       +                        window_button_release(view->window, &e);
       +                        break;
       +                case 'm':
       +                        /* mouse motion */
       +                        /* m x y */
       +                        if (read_int(&x, 0))
       +                                goto error;
       +                        if (read_int(&y, 1))
       +                                goto error;
       +                        e = (XEvent){.xmotion = {.type = MotionNotify, .x = x, .y = y}};
       +                        window_register_motion(view->window, &e);
       +                        break;
       +                case 'l':
       +                        /* language switch */
       +                        /* l lang_name */
       +                        if (read_text(&text, &text_len))
       +                                goto error;
       +                        term = ledit_strndup(text, text_len);
       +                        change_keyboard(term);
       +                        free(term);
       +                        break;
       +                case 's':
       +                        /* switch view */
       +                        /* s view_num */
       +                        if (read_uint(&new_view, 1))
       +                                goto error;
       +                        if (new_view >= buffer->views_num)
       +                                fprintf(stderr, "Invalid view number %u\n", new_view);
       +                        else
       +                                view_num = new_view;
       +                        break;
       +                case 'w':
       +                        /* write contents of buffer */
       +                        /* w file_name */
       +                        if (read_filename(&text, &text_len))
       +                                goto error;
       +                        term = ledit_strndup(text, text_len);
       +                        if (buffer_write_to_filename(buffer, term, &errstr))
       +                                fprintf(stderr, "Error writing %s: %s\n", term, errstr);
       +                        free(term);
       +                        break;
       +                case 'd':
       +                        /* dump other info to file */
       +                        /* d file_name */
       +                        if (read_filename(&text, &text_len))
       +                                goto error;
       +                        term = ledit_strndup(text, text_len);
       +                        file = fopen(term, "w");
       +                        if (!file) {
       +                                fprintf(stderr, "Unable to open file %s\n", term);
       +                        } else {
       +                                fprintf(
       +                                    file,
       +                                    "cursor_line: %zu, cursor_byte: %zu, sel_valid: %d, "
       +                                    "sel_line1: %zu, sel_byte1: %zu, "
       +                                    "sel_line2: %zu, sel_byte2: %zu\n",
       +                                    view->cur_line, view->cur_index, view->sel_valid,
       +                                    view->sel.line1, view->sel.byte1,
       +                                    view->sel.line2, view->sel.byte2
       +                                );
       +                                fclose(file);
       +                        }
       +                        free(term);
       +                        break;
       +                case 'u':
       +                        /* dump undo stack to file */
       +                        if (read_filename(&text, &text_len))
       +                                goto error;
       +                        /* u file_name */
       +                        term = ledit_strndup(text, text_len);
       +                        file = fopen(term, "w");
       +                        if (!file) {
       +                                fprintf(stderr, "Unable to open file %s\n", term);
       +                        } else {
       +                                dump_undo_stack(file, buffer->undo);
       +                                fclose(file);
       +                        }
       +                        free(term);
       +                        break;
       +                default:
       +                        goto error;
       +        }
       +        return 1;
       +error:
       +        fprintf(stderr, "Error parsing command.\n");
       +        test_status.read_cur = nl_index + 1;
       +        return 1;
       +}
       +#endif
       +
       +/* can only be set to 1 when compiled with TEST */
       +static int test_extra = 0;
       +
        static void
        mainloop(void) {
       +        #if TEST
       +        int flags = fcntl(fileno(stdin), F_GETFL, 0);
       +        if (flags == -1) {
       +                fprintf(stderr, "Unable to set non-blocking mode on stdin.\n");
       +                return;
       +        }
       +        if (fcntl(fileno(stdin), F_SETFL, flags | O_NONBLOCK)) {
       +                fprintf(stderr, "Unable to set non-blocking mode on stdin.\n");
       +                return;
       +        }
       +        #endif
                XEvent event;
                int xkb_event_type;
                int major, minor;
       t@@ -138,16 +448,20 @@ mainloop(void) {
                                        window_register_resize(view->window, &event);
                                        break;
                                case ButtonPress:
       -                                window_register_button_press(view->window, &event);
       +                                if (!test_extra)
       +                                        window_register_button_press(view->window, &event);
                                        break;
                                case ButtonRelease:
       -                                window_button_release(view->window, &event);
       +                                if (!test_extra)
       +                                        window_button_release(view->window, &event);
                                        break;
                                case MotionNotify:
       -                                window_register_motion(window, &event);
       +                                if (!test_extra)
       +                                        window_register_motion(window, &event);
                                        break;
                                case KeyPress:
       -                                key_press(view, &event);
       +                                if (!test_extra)
       +                                        key_press_event(view, &event);
                                        break;
                                case ClientMessage:
                                        if ((Atom)event.xclient.data.l[0] == view->window->wm_delete_msg) {
       t@@ -161,11 +475,22 @@ mainloop(void) {
                                }
                        };
        
       +                #if TEST
       +                int ret;
       +                if ((ret = read_input()) == 1) {
       +                        fprintf(stderr, "Unable to read text from stdin.\n");
       +                } else if (ret == 0) {
       +                        while (process_commands()) {
       +                                /* NOP */
       +                        }
       +                }
       +                #endif
       +
                        for (size_t i = 0; i < buffer->views_num; i++) {
                                window_handle_filtered_events(buffer->views[i]->window);
                        }
        
       -                if (change_kbd) {
       +                if (!test_extra && change_kbd) {
                                change_kbd = 0;
                                XkbStateRec s;
                                XkbGetState(common.dpy, XkbUseCoreKbd, &s);
       t@@ -201,11 +526,21 @@ setup(int argc, char *argv[]) {
        
                char c;
                char *opt_filename = NULL;
       -        while ((c = getopt(argc, argv, "c:")) != -1) {
       +        #if TEST
       +        char *opts = "tc:";
       +        #else
       +        char *opts = "c:";
       +        #endif
       +        while ((c = getopt(argc, argv, opts)) != -1) {
                        switch (c) {
                        case 'c':
                                opt_filename = optarg;
                                break;
       +                #if TEST
       +                case 't':
       +                        test_extra = 1;
       +                        break;
       +                #endif
                        default:
                                fprintf(stderr, "USAGE: ledit [-c config] [file]\n");
                                exit(1);
       t@@ -517,16 +852,26 @@ change_keyboard(char *lang) {
        }
        
        static void
       -key_press(ledit_view *view, XEvent *event) {
       +key_press(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int n) {
                /* FIXME: just let view handle this since the action is part
                   of it anyways now */
                if (view->cur_action.type == ACTION_GRABKEY && view->cur_action.callback) {
       -                view->cur_action = view->cur_action.callback(view, event, cur_lang);
       +                view->cur_action = view->cur_action.callback(view, key_state, sym, buf, n, cur_lang);
                } else {
       -                view->cur_action = basic_key_handler(view, event, cur_lang);
       +                view->cur_action = basic_key_handler(view, key_state, sym, buf, n, cur_lang);
                }
        }
        
       +static void
       +key_press_event(ledit_view *view, XEvent *event) {
       +        char *buf = NULL;
       +        KeySym sym = NoSymbol;
       +        int n;
       +        unsigned int key_state = event->xkey.state;
       +        preprocess_key(view->window, &event->xkey, &sym, &buf, &n);
       +        key_press(view, key_state, sym, buf, n);
       +}
       +
        int
        main(int argc, char *argv[]) {
                setup(argc, argv);
   DIR diff --git a/tests/README b/tests/README
       t@@ -0,0 +1,59 @@
       +There aren't any proper tests currently, but some infrastructure is in place to support them.
       +
       +When compiled with TEST=1, ledit accepts commands on standard input to generate fake events.
       +Each command ends in newline. If the last argument is text, it may also contain newlines if
       +they are escaped with backslash. Single backslashes that are not in front of a newline are
       +just taken verbatim, but two backslashes are collapsed into one. Filenames are a special
       +case because they are not allowed to contain '/' or '\0'.
       +
       +The commands to generate events take raw integers instead of symbolic names for keysyms
       +and other parameters. These need to be given using the definitions from Xlib.
       +
       +The commands currently supported are the following:
       +
       +k <key_state> <keysym> <text>
       +
       +Generate a keypress event. <key_state> and <keysym> are the modifier state and keysym.
       +
       +p <button_num> <x> <y>
       +
       +Generate a mouse button press event.
       +
       +r <button_num> <x> <y>
       +
       +Generate a mouse button release event.
       +
       +m <x> <y>
       +
       +Generate a mouse motion event.
       +
       +l <lang>
       +
       +Switch to keyboard layout <lang>.
       +
       +s <view_num>
       +
       +Switch to view <view_num>, if it exists.
       +
       +w <filename>
       +
       +Write the contents of the buffer to <filename>.
       +
       +d <filename>
       +
       +Dump various information to <filename>. Currently, the cursor position and
       +information about the selection is given. See ledit.c for the exact format.
       +
       +u <filename>
       +
       +Dump the undo stack to <filename>. See undo.c for the exact format.
       +
       +TODO: Add more commands, e.g. for dumping the repetition stack.
       +
       +
       +When compiled with TEST=1, ledit supports an additional command-line argument '-t'.
       +This disables handling of the regular key press, mouse, and language switch events
       +in order to avoid messing with the results.
       +
       +Note that regular file writing using :w is disabled when compiled with TEST=1
       +in order to avoid overwriting anything important if fuzz testing is done.
   DIR diff --git a/undo.c b/undo.c
       t@@ -101,22 +101,27 @@ undo_change_mode_group(undo_stack *undo) {
                undo->change_mode_group = 1;
        }
        
       -/*
       -static void
       -dump_undo(undo_stack *undo) {
       -        printf("START UNDO STACK\n");
       -        printf("cur: %zu\n", undo->cur);
       +#if TEST
       +void
       +dump_undo_stack(FILE *file, undo_stack *undo) {
       +        fprintf(
       +            file,
       +            "cur: %zu, cur_valid: %d, change_mode_group: %d, len: %zu, cap: %zu\n",
       +            undo->cur, undo->cur_valid, undo->change_mode_group, undo->len, undo->cap
       +        );
                for (size_t i = 0; i < undo->len; i++) {
                        undo_elem *e = &undo->stack[i];
       -                printf(
       -                    "type %d, mode %d, group %d, mode_group %d, text '%.*s', range (%zu, %zu)\n",
       +                fprintf(
       +                    file,
       +                    "type %d, mode %d, group %d, mode_group %d, text '%.*s', "
       +                    "op_range (%zu,%zu;%zu,%zu), cursor_range (%zu,%zu;%zu,%zu)\n",
                            e->type, e->mode, e->group, e->mode_group, (int)e->text->len, e->text->text,
       -                    e->op_range.byte1, e->op_range.byte2
       +                    e->op_range.line1, e->op_range.byte1, e->op_range.line2, e->op_range.byte2,
       +                    e->cursor_range.line1, e->cursor_range.byte1, e->cursor_range.line2, e->cursor_range.byte2
                        );
                }
       -        printf("END UNDO STACK\n");
        }
       -*/
       +#endif
        
        static void
        push_undo(
       t@@ -140,7 +145,6 @@ push_undo(
                        txtbuf_copy(e->text, text);
                else
                        e->text = txtbuf_dup(text);
       -        /* dump_undo(undo); */
        }
        
        void
       t@@ -243,7 +247,6 @@ ledit_undo(undo_stack *undo, ledit_mode mode, void *callback_data,
                *min_line_ret = min_line;
                if (mode == NORMAL || mode == VISUAL)
                        undo_change_mode_group(undo);
       -        /* dump_undo(undo); */
                return UNDO_NORMAL;
        }
        
   DIR diff --git a/undo.h b/undo.h
       t@@ -133,4 +133,8 @@ void undo_change_last_cur_range(undo_stack *undo, ledit_range cur_range);
         */
        char *undo_state_to_str(undo_status s);
        
       +#if TEST
       +void dump_undo_stack(FILE *file, undo_stack *undo);
       +#endif
       +
        #endif
   DIR diff --git a/view.h b/view.h
       t@@ -26,7 +26,7 @@ enum action_type {
           main event manager what key handler to call next */
        struct action {
                enum action_type type;
       -        struct action (*callback)(ledit_view *view, XEvent *event, int lang_index);
       +        struct action (*callback)(ledit_view *view, unsigned int key_state, KeySym sym, char *buf, int n, int lang_index);
        };
        
        typedef struct {