diff options
Diffstat (limited to 'tests/test_input.cpp')
| -rw-r--r-- | tests/test_input.cpp | 1263 |
1 files changed, 1263 insertions, 0 deletions
diff --git a/tests/test_input.cpp b/tests/test_input.cpp new file mode 100644 index 0000000..99a719f --- /dev/null +++ b/tests/test_input.cpp @@ -0,0 +1,1263 @@ +#include "input.h" +#include "util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond, msg) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ + } \ + } while (0) + +#define CHECK_CONT(hay, needle, msg) \ + do { \ + checks++; \ + if (!hay || !strstr(hay, needle)) { \ + failures++; \ + fprintf(stderr, \ + "FAIL %s:%d: %s (got `%s`, want substring `%s`)\n", \ + __FILE__, __LINE__, (msg), hay ? hay : "(null)", \ + (needle)); \ + } \ + } while (0) + +#define CHECK_NOT_CONT(hay, needle, msg) \ + do { \ + checks++; \ + if (hay && strstr(hay, needle)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s (found `%s` in `%s`)\n", \ + __FILE__, __LINE__, (msg), (needle), hay); \ + } \ + } while (0) + +#define CHECK_STR(got, want, msg) \ + do { \ + checks++; \ + if (!got || strcmp(got, want) != 0) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s (got `%s`, want `%s`)\n", \ + __FILE__, __LINE__, (msg), got ? got : "(null)", (want));\ + } \ + } while (0) + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +static input_key kkind(int kind) { + input_key k; + k.kind = kind; + k.cp = 0; + return k; +} + +static evlist feed(const unsigned char *bytes, size_t n) { + key_parser p; + evlist ev; + key_parser_init(&p); + evlist_init(&ev); + key_parser_feed(&p, bytes, n, &ev); + key_parser_flush(&p, &ev); + key_parser_free(&p); + return ev; +} + +/* Apply a run of parsed keys; every action must be ACT_NONE. */ +static void type_str(editor *e, const char *s) { + evlist ev = feed((const unsigned char *)s, strlen(s)); + for (size_t i = 0; i < ev.len; i++) { + input_action a; + editor_apply(e, ev.items[i], &a); + CHECK(a.kind == ACT_NONE, "type_str: unexpected action while typing"); + input_action_free(&a); + } + evlist_free(&ev); +} + +/* Apply a single named key and release its action. */ +static void apply_key(editor *e, int kind) { + input_action a; + editor_apply(e, kkind(kind), &a); + input_action_free(&a); +} + +/* ================================================================== */ +/* Parser tests */ +/* ================================================================== */ + +static void test_parser_plain_chars(void) { + evlist ev = feed((const unsigned char *)"abc", 3); + CHECK(ev.len == 3, "three chars"); + if (ev.len == 3) { + CHECK(ev.items[0].kind == KEY_CHAR && ev.items[0].cp == 'a', "a"); + CHECK(ev.items[1].kind == KEY_CHAR && ev.items[1].cp == 'b', "b"); + CHECK(ev.items[2].kind == KEY_CHAR && ev.items[2].cp == 'c', "c"); + CHECK(ev.raws[0] && ev.lens[0] == 1 && ev.raws[0][0] == 'a', "raw a"); + CHECK(ev.raws[2] && ev.lens[2] == 1 && ev.raws[2][0] == 'c', "raw c"); + } + evlist_free(&ev); +} + +static void test_parser_enter_variants(void) { + evlist ev = feed((const unsigned char *)"\r", 1); + CHECK(ev.len == 1 && ev.items[0].kind == KEY_ENTER, "CR -> Enter"); + evlist_free(&ev); + + /* LF (Ctrl+J) is a dead key outside of paste: no event. */ + ev = feed((const unsigned char *)"\n", 1); + CHECK(ev.len == 0, "LF -> no event (dead key)"); + evlist_free(&ev); +} + +/* Shift-modified Enter and Esc carry no special binding: they behave as the + * plain key. */ +static void test_parser_shift_enter_and_esc(void) { + evlist ev = feed((const unsigned char *)"\x1b[13;2u", 7); + CHECK(ev.len == 1 && ev.items[0].kind == KEY_ENTER, "CSI 13;2 -> Enter"); + evlist_free(&ev); + + ev = feed((const unsigned char *)"\x1b[27;2u", 7); + CHECK(ev.len == 1 && ev.items[0].kind == KEY_ESC, "CSI 27;2 -> Esc"); + evlist_free(&ev); +} + +static void test_parser_lone_escape_is_esc(void) { + /* a lone ESC is held until flush */ + key_parser p; + evlist ev; + key_parser_init(&p); + evlist_init(&ev); + key_parser_feed(&p, (const unsigned char *)"\x1b", 1, &ev); + CHECK(ev.len == 0, "no event before flush"); + CHECK(key_parser_flush(&p, &ev) == 1, "flush emits one"); + CHECK(ev.len == 1 && ev.items[0].kind == KEY_ESC, "flush -> Esc"); + evlist_free(&ev); + key_parser_free(&p); +} + +static void test_parser_alt_letter_is_the_letter(void) { + evlist ev = feed((const unsigned char *)"\x1b" "a", 2); + CHECK(ev.len == 1 && ev.items[0].kind == KEY_CHAR && ev.items[0].cp == 'a', + "alt+a -> Char('a')"); + if (ev.len == 1) { + CHECK(ev.raws[0] && ev.lens[0] == 2 && ev.raws[0][0] == '\x1b' && + ev.raws[0][1] == 'a', + "raw = ESC a"); + } + evlist_free(&ev); +} + +static void test_parser_raw_bytes_preserved(void) { + evlist ev = feed((const unsigned char *)"\x1b[1;5A", 6); + CHECK(ev.len == 1 && ev.items[0].kind == KEY_UP, "UP"); + if (ev.len == 1) { + CHECK(ev.raws[0] && ev.lens[0] == 6, "raw len 6"); + if (ev.raws[0] && ev.lens[0] == 6) + CHECK(memcmp(ev.raws[0], "\x1b[1;5A", 6) == 0, "raw bytes"); + } + evlist_free(&ev); +} + +static void test_parser_control_keys(void) { + struct { + unsigned char b; + int want; + const char *m; + } cases[] = { + {0x01, KEY_CTRL_A, "Ctrl-A"}, + {0x03, KEY_CTRL_C, "Ctrl-C"}, + {0x04, KEY_CTRL_D, "Ctrl-D"}, + {0x05, KEY_CTRL_E, "Ctrl-E"}, + {0x0B, KEY_CTRL_K, "Ctrl-K"}, + {0x0C, KEY_CTRL_L, "Ctrl-L"}, + {0x15, KEY_CTRL_U, "Ctrl-U"}, + {0x17, KEY_CTRL_W, "Ctrl-W"}, + {0x19, KEY_CTRL_Y, "Ctrl-Y"}, + {0x1F, KEY_UNDO, "Ctrl-_ (undo)"}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + evlist ev = feed(&cases[i].b, 1); + CHECK(ev.len == 1, cases[i].m); + if (ev.len == 1) { + checks++; + if (ev.items[0].kind != cases[i].want) { + failures++; + fprintf(stderr, "FAIL %s:%d: %s (got %d want %d)\n", __FILE__, + __LINE__, cases[i].m, ev.items[0].kind, cases[i].want); + } + } + evlist_free(&ev); + } + + unsigned char bs[2] = {0x7F, 0x08}; + for (int i = 0; i < 2; i++) { + evlist ev = feed(&bs[i], 1); + CHECK(ev.len == 1 && ev.items[0].kind == KEY_BACKSPACE, "backspace"); + evlist_free(&ev); + } +} + +static void test_parser_unmapped_control_bytes(void) { + int mapped[256] = {0}; + mapped[0x09] = mapped[0x0A] = mapped[0x0D] = mapped[0x1B] = 1; + for (int b = 0; b < 0x20; b++) { + if (mapped[b]) + continue; + unsigned char c = (unsigned char)b; + evlist ev = feed(&c, 1); + CHECK(ev.len == 1, "exactly one control event"); + if (ev.len == 1) + CHECK(ev.items[0].kind != KEY_CHAR, "not a char"); + evlist_free(&ev); + } +} + +static void test_parser_unknown_sequence_consumed(void) { + // Unmapped CSI tilde: empty or a single Unknown (oracle allows either). + evlist ev = feed((const unsigned char *)"\x1b[999~", 6); + CHECK(ev.len == 0 || (ev.len == 1 && ev.items[0].kind == KEY_UNKNOWN), + "unknown CSI ~"); + evlist_free(&ev); + + // Empty bracketed paste: markers consumed, no events. + ev = feed((const unsigned char *)"\x1b[200~\x1b[201~", 12); + CHECK(ev.len == 0, "empty paste -> no events"); + evlist_free(&ev); + + // Mouse reports must not surface as editor keys. + ev = feed((const unsigned char *)"\x1b[M", 3); + CHECK(ev.len == 0, "mouse SGR -> no events"); + evlist_free(&ev); + + ev = feed((const unsigned char *)"\x1b[64;10;5M", 10); + CHECK(ev.len == 0, "mouse CSI -> no events"); + evlist_free(&ev); + + // Cursor position reports (DSR responses) are consumed. + ev = feed((const unsigned char *)"\x1b[12;40R", 8); + CHECK(ev.len == 0, "DSR report -> no events"); + evlist_free(&ev); +} + +static void test_parser_bracketed_paste(void) { + // Paste start marker: no event on its own. + evlist ev = feed((const unsigned char *)"\x1b[200~", 6); + CHECK(ev.len == 0, "paste start -> no event"); + evlist_free(&ev); + + // Pasted content is individual char events; a pasted CR is a Newline. + ev = feed((const unsigned char *)"\x1b[200~hello\rworld\x1b[201~", 23); + CHECK(ev.len == 11, "paste content = 11 events"); + if (ev.len == 11) { + CHECK(ev.items[0].kind == KEY_CHAR && ev.items[0].cp == 'h', "h"); + CHECK(ev.items[4].kind == KEY_CHAR && ev.items[4].cp == 'o', "o"); + CHECK(ev.items[5].kind == KEY_NEWLINE, "pasted CR -> Newline"); + CHECK(ev.items[10].kind == KEY_CHAR && ev.items[10].cp == 'd', "d"); + } + evlist_free(&ev); +} + +static void test_parser_utf8(void) { + evlist ev = feed((const unsigned char *)"h\xc3\xa9llo", 6); + CHECK(ev.len == 5, "five codepoints"); + if (ev.len == 5) { + CHECK(ev.items[0].kind == KEY_CHAR && ev.items[0].cp == 'h', "h"); + CHECK(ev.items[1].kind == KEY_CHAR && ev.items[1].cp == 0xE9, "e-acute"); + CHECK(ev.items[4].kind == KEY_CHAR && ev.items[4].cp == 'o', "o"); + } + evlist_free(&ev); + + /* split across feed() calls */ + key_parser p; + evlist ev2; + key_parser_init(&p); + evlist_init(&ev2); + key_parser_feed(&p, (const unsigned char *)"h\xc3", 2, &ev2); + CHECK(ev2.len == 1 && ev2.items[0].kind == KEY_CHAR && + ev2.items[0].cp == 'h', + "h flushed, partial UTF-8 held"); + key_parser_feed(&p, (const unsigned char *)"\xa9llo", 4, &ev2); + key_parser_flush(&p, &ev2); + CHECK(ev2.len == 5, "recombined to five codepoints"); + evlist_free(&ev2); + key_parser_free(&p); + + evlist ev3 = feed((const unsigned char *)"\xff", 1); + CHECK(ev3.len == 1 && ev3.items[0].kind == KEY_UNKNOWN, "invalid 0xFF"); + evlist_free(&ev3); +} + +static void test_parser_mixed_stream(void) { + unsigned char seq[] = "a\x1b[1;5A\xc3\xa9\x1b[13u"; + evlist ev = feed(seq, sizeof(seq) - 1); + CHECK(ev.len == 4, "four events"); + if (ev.len == 4) { + CHECK(ev.items[0].kind == KEY_CHAR && ev.items[0].cp == 'a', "a"); + CHECK(ev.items[1].kind == KEY_UP, "UP"); + CHECK(ev.items[2].kind == KEY_CHAR && ev.items[2].cp == 0xE9, "e-acute"); + CHECK(ev.items[3].kind == KEY_ENTER, "Enter (CSI 13)"); + } + evlist_free(&ev); +} + +static void test_parser_kitty_and_ss3(void) { + struct { + const char *seq; + int want; + const char *m; + } cases[] = { + {"\x1b[8593;1u", KEY_UP, "kitty up"}, + {"\x1b[8594;1u", KEY_RIGHT, "kitty right"}, + {"\x1b[9u", KEY_TAB, "kitty tab"}, + {"\x1b[9;2u", KEY_BACKTAB, "kitty backtab"}, + {"\x1b[12u", KEY_BACKSPACE, "kitty backspace"}, + {"\x1b[127u", KEY_DELETE, "kitty delete"}, + {"\x1b[13u", KEY_ENTER, "kitty enter"}, + {"\x1b[27u", KEY_ESC, "kitty esc"}, + {"\x1bOA", KEY_UP, "ss3 up"}, + {"\x1bOD", KEY_LEFT, "ss3 left"}, + {"\x1b[H", KEY_HOME, "csi home"}, + {"\x1b[3~", KEY_DELETE, "csi delete"}, + {"\x1b[4~", KEY_END, "csi end"}, + {"\x1b[5~", KEY_PAGEUP, "csi pageup"}, + {"\x1b[6~", KEY_PAGEDOWN, "csi pagedown"}, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + const char *seq = cases[i].seq; + evlist ev = feed((const unsigned char *)seq, strlen(seq)); + CHECK(ev.len == 1, cases[i].m); + if (ev.len == 1) { + checks++; + if (ev.items[0].kind != cases[i].want) { + failures++; + fprintf(stderr, "FAIL %s:%d: %s (got %d want %d)\n", __FILE__, + __LINE__, cases[i].m, ev.items[0].kind, cases[i].want); + } + } + evlist_free(&ev); + } +} + +/* ================================================================== */ +/* Editor tests */ +/* ================================================================== */ + +static void test_editor_typing_and_submit(void) { + editor e; + editor_init(&e); + type_str(&e, "hello"); + char *t = editor_text(&e); + CHECK_STR(t, "hello", "text after typing"); + free(t); + + input_action a; + editor_apply(&e, kkind(KEY_ENTER), &a); + CHECK(a.kind == ACT_SUBMIT, "Enter submits"); + if (a.kind == ACT_SUBMIT) + CHECK_STR(a.text, "hello", "submitted text"); + input_action_free(&a); + editor_free(&e); +} + +static void test_editor_shift_enter_inserts_newline(void) { + editor e; + editor_init(&e); + type_str(&e, "he"); + apply_key(&e, KEY_NEWLINE); + type_str(&e, "llo"); + char *t = editor_text(&e); + CHECK_STR(t, "he\nllo", "NEWLINE key inserts a newline"); + free(t); + CHECK(editor_line_count(&e) == 2, "two lines"); + + input_action a; + editor_apply(&e, kkind(KEY_ENTER), &a); + CHECK(a.kind == ACT_SUBMIT, "Enter submits"); + if (a.kind == ACT_SUBMIT) + CHECK_STR(a.text, "he\nllo", "submitted multi-line text"); + input_action_free(&a); + editor_free(&e); +} + +/* Backslash + Enter at the end of the buffer is a line continuation (a + * universal newline trigger for terminals where Shift+Enter is not + * distinguishable from Enter); any other Enter still submits. */ +static void test_editor_backslash_enter_continues(void) { + editor e; + editor_init(&e); + type_str(&e, "ls \\"); + input_action a; + editor_apply(&e, kkind(KEY_ENTER), &a); + CHECK(a.kind == ACT_NEWLINE, "trailing backslash + Enter -> continuation"); + input_action_free(&a); + char *t = editor_text(&e); + CHECK_STR(t, "ls \n", "backslash became a newline"); + free(t); + CHECK(e.caret == 4, "caret after the newline"); + + type_str(&e, "-la"); + editor_apply(&e, kkind(KEY_ENTER), &a); + CHECK(a.kind == ACT_SUBMIT, "second line's Enter submits"); + if (a.kind == ACT_SUBMIT) + CHECK_STR(a.text, "ls \n-la", "multi-line text submitted"); + input_action_free(&a); + editor_free(&e); + + /* No backslash before the caret -> Enter still submits. */ + editor f; + editor_init(&f); + type_str(&f, "a\\b"); + input_action b; + editor_apply(&f, kkind(KEY_ENTER), &b); + CHECK(b.kind == ACT_SUBMIT, "no backslash before caret submits"); + if (b.kind == ACT_SUBMIT) + CHECK_STR(b.text, "a\\b", "text submitted unchanged"); + input_action_free(&b); + editor_free(&f); + + /* A backslash before the caret continues even with text after it. */ + editor g; + editor_init(&g); + type_str(&g, "a\\b"); + apply_key(&g, KEY_LEFT); /* caret sits right after the backslash */ + input_action c; + editor_apply(&g, kkind(KEY_ENTER), &c); + CHECK(c.kind == ACT_NEWLINE, "backslash before caret continues"); + input_action_free(&c); + char *gt = editor_text(&g); + CHECK_STR(gt, "a\nb", "newline inserted after the backslash"); + free(gt); + editor_free(&g); +} + +/* Regression: after building a multi-line buffer and moving the caret UP to + * a middle line, backslash+Enter on that line must split it (insert a + * newline), not submit. Previously the continuation only fired when the + * caret was at the very end of the whole buffer. */ +static void test_editor_middle_line_continuation(void) { + editor e; + editor_init(&e); + /* Build "alpha \nbeta": line 1 "alpha ", line 2 "beta". */ + type_str(&e, "alpha "); + type_str(&e, "\\"); + apply_key(&e, KEY_ENTER); /* "alpha \" -> "alpha \n" */ + type_str(&e, "beta"); /* -> "alpha \nbeta", caret at end */ + CHECK(editor_line_count(&e) == 2, "two lines before the split"); + /* Move the caret UP to the middle (first) line. */ + apply_key(&e, KEY_UP); + /* Walk to the end of that line (just before the newline). */ + while (e.caret < strlen(e.text) && e.text[e.caret] != '\n') + apply_key(&e, KEY_RIGHT); + CHECK(e.caret == 6, "caret at end of the middle line"); + /* Type a backslash on the middle line, then Enter -> split it. */ + type_str(&e, "\\"); + input_action a; + editor_apply(&e, kkind(KEY_ENTER), &a); + CHECK(a.kind == ACT_NEWLINE, "middle-line backslash+Enter continues"); + input_action_free(&a); + char *t = editor_text(&e); + CHECK_STR(t, "alpha \n\nbeta", "middle line was split"); + free(t); + CHECK(editor_line_count(&e) == 3, "three lines after the split"); + + /* And the buffer still submits as a whole from its last line. */ + apply_key(&e, KEY_DOWN); + while (e.caret < strlen(e.text) && e.text[e.caret] != '\n') + apply_key(&e, KEY_RIGHT); + editor_apply(&e, kkind(KEY_ENTER), &a); + CHECK(a.kind == ACT_SUBMIT, "last-line Enter submits"); + if (a.kind == ACT_SUBMIT) + CHECK_STR(a.text, "alpha \n\nbeta", "multi-line submit intact"); + input_action_free(&a); + editor_free(&e); +} + +/* End-to-end regression: Ctrl+J is LF (0x0A). Outside of bracketed paste it + * is a dead key — it neither submits nor inserts a newline. Multi-line input + * is via backslash+Enter or bracketed paste. */ +static void test_ctrl_j_is_dead_key(void) { + key_parser p; + evlist ev; + key_parser_init(&p); + evlist_init(&ev); + key_parser_feed(&p, (const unsigned char *)"hi\n", 3, &ev); + key_parser_free(&p); + CHECK(ev.len == 2, "only the two chars, LF dropped"); + + editor e; + editor_init(&e); + input_action a = {ACT_NONE, NULL}; + int submitted = 0; + for (size_t i = 0; i < ev.len; i++) { + editor_apply(&e, ev.items[i], &a); + if (a.kind == ACT_SUBMIT) + submitted = 1; + input_action_free(&a); + } + CHECK(!submitted, "Ctrl+J (LF) does not submit"); + char *t = editor_text(&e); + CHECK_STR(t, "hi", "LF left no newline in the buffer"); + free(t); + evlist_free(&ev); + editor_free(&e); +} + +static void test_editor_backspace_and_delete(void) { + editor e; + editor_init(&e); + type_str(&e, "he"); + apply_key(&e, KEY_NEWLINE); + type_str(&e, "llo"); + apply_key(&e, KEY_LEFT); + apply_key(&e, KEY_LEFT); + apply_key(&e, KEY_LEFT); + apply_key(&e, KEY_BACKSPACE); + char *t = editor_text(&e); + CHECK_STR(t, "hello", "backspace removed the newline"); + CHECK(e.caret == 2, "caret at the join"); + free(t); + editor_free(&e); + + /* A forward-delete needs the caret at the end of the first line: Up gets + * there (Left is now clamped to a line), then Delete removes the newline. */ + editor f; + editor_init(&f); + type_str(&f, "ab"); + apply_key(&f, KEY_NEWLINE); + type_str(&f, "cd"); + apply_key(&f, KEY_UP); + apply_key(&f, KEY_DELETE); + char *u = editor_text(&f); + CHECK_STR(u, "abcd", "delete removed the newline"); + CHECK(f.caret == 2, "caret at the join"); + free(u); + editor_free(&f); +} + +static void test_editor_multibyte_backspace(void) { + editor e; + editor_init(&e); + type_str(&e, "h\xc3\xa9llo"); + apply_key(&e, KEY_END); + apply_key(&e, KEY_BACKSPACE); + apply_key(&e, KEY_BACKSPACE); + apply_key(&e, KEY_BACKSPACE); + apply_key(&e, KEY_BACKSPACE); + char *t = editor_text(&e); + CHECK_STR(t, "h", "removed whole codepoints"); + free(t); + editor_free(&e); +} + +static void test_editor_horizontal_movement(void) { + editor e; + editor_init(&e); + type_str(&e, "one"); + apply_key(&e, KEY_NEWLINE); + type_str(&e, "two"); + CHECK(e.caret == 7, "caret at the end"); + + /* Left stays on the current line: three presses reach its start, and a + * fourth is a no-op (it no longer crosses into the previous line). */ + apply_key(&e, KEY_LEFT); + apply_key(&e, KEY_LEFT); + apply_key(&e, KEY_LEFT); + CHECK(e.caret == 4, "left moves the caret to the line start"); + apply_key(&e, KEY_LEFT); + CHECK(e.caret == 4, "left is a no-op at the line start"); + apply_key(&e, KEY_RIGHT); + CHECK(e.caret == 5, "right moves the caret"); + + /* HOME/END (and their Ctrl aliases) act on the current line, not the + * whole buffer. Move to the first line and exercise them there. */ + apply_key(&e, KEY_UP); + CHECK(editor_caret_line(&e) == 0, "on the first line"); + apply_key(&e, KEY_HOME); + CHECK(e.caret == 0, "home is the line start"); + apply_key(&e, KEY_END); + CHECK(e.caret == 3, "end is the line end"); + apply_key(&e, KEY_CTRL_A); + CHECK(e.caret == 0, "ctrl-a is the line start"); + apply_key(&e, KEY_CTRL_E); + CHECK(e.caret == 3, "ctrl-e is the line end"); + editor_free(&e); +} + +/* Vertical movement walks the lines, keeps the column, clamps it to short + * lines on a UTF-8 boundary, and stops at the top/bottom edges. */ +static void test_editor_vertical_movement(void) { + editor e; + editor_init(&e); + editor_set(&e, "one\ntwo\nthree"); + CHECK(editor_line_count(&e) == 3, "three lines"); + CHECK(editor_caret_line(&e) == 2, "caret on the last line"); + CHECK(e.caret == 13, "caret at the end"); + + apply_key(&e, KEY_UP); + CHECK(e.caret == 7, "up to the end of line 2"); + CHECK(editor_caret_line(&e) == 1, "caret on line 2"); + apply_key(&e, KEY_UP); + CHECK(e.caret == 3, "up to the end of line 1"); + CHECK(editor_caret_line(&e) == 0, "caret on line 1"); + apply_key(&e, KEY_UP); + CHECK(e.caret == 3, "up is a no-op on the first line"); + + apply_key(&e, KEY_DOWN); + CHECK(e.caret == 7, "down back to line 2"); + apply_key(&e, KEY_DOWN); + CHECK(e.caret == 11, "down into line 3, column preserved"); + CHECK(editor_caret_line(&e) == 2, "caret on line 3"); + apply_key(&e, KEY_DOWN); + CHECK(e.caret == 11, "down is a no-op on the last line"); + editor_free(&e); + + /* Column clamping: a column longer than the target line clamps to its + * end, and the clamped column is what comes back. */ + editor f; + editor_init(&f); + editor_set(&f, "abcdef\ngh"); + CHECK(f.caret == 9, "set caret at the end"); + apply_key(&f, KEY_UP); + CHECK(f.caret == 2, "up keeps the column"); + apply_key(&f, KEY_RIGHT); + apply_key(&f, KEY_RIGHT); + apply_key(&f, KEY_RIGHT); + apply_key(&f, KEY_RIGHT); + CHECK(f.caret == 6, "end of the long line"); + apply_key(&f, KEY_DOWN); + CHECK(f.caret == 9, "down clamps the column to the short line"); + apply_key(&f, KEY_UP); + CHECK(f.caret == 2, "up keeps the clamped column"); + editor_free(&f); + + /* A single-line buffer: UP/DOWN never move the caret. */ + editor h; + editor_init(&h); + editor_set(&h, "hello"); + apply_key(&h, KEY_HOME); + apply_key(&h, KEY_UP); + CHECK(h.caret == 0, "UP is a no-op on one line"); + apply_key(&h, KEY_DOWN); + CHECK(h.caret == 0, "DOWN is a no-op on one line"); + editor_free(&h); + + /* UTF-8: a column landing mid-codepoint snaps to the boundary. */ + editor u; + editor_init(&u); + editor_set(&u, "abc\n\xc3\xa9\xc3\xa9"); /* "abc", then two e-acute */ + CHECK(u.caret == 8, "caret at the end"); + apply_key(&u, KEY_UP); + CHECK(u.caret == 3, "up to the end of line 1"); + apply_key(&u, KEY_DOWN); + CHECK(u.caret == 6, "down clamps between the two codepoints"); + apply_key(&u, KEY_UP); + CHECK(u.caret == 2, "up keeps the clamped column"); + editor_free(&u); +} + +static void test_editor_insert_at_caret(void) { + editor e; + editor_init(&e); + type_str(&e, "helo"); + apply_key(&e, KEY_HOME); + apply_key(&e, KEY_RIGHT); + type_str(&e, "i"); + char *t = editor_text(&e); + CHECK_STR(t, "hielo", "inserted before the caret"); + CHECK(e.caret == 2, "caret after the inserted char"); + free(t); + editor_free(&e); + + editor f; + editor_init(&f); + apply_key(&f, KEY_BACKSPACE); + CHECK(editor_is_empty(&f), "backspace at 0 is a no-op"); + apply_key(&f, KEY_DELETE); + CHECK(editor_is_empty(&f), "delete at 0 is a no-op"); + editor_free(&f); +} + +static void test_editor_kill_and_word_kills(void) { + editor e; + editor_init(&e); + type_str(&e, "foo bar"); + apply_key(&e, KEY_CTRL_K); + char *t = editor_text(&e); + CHECK_STR(t, "foo bar", "Ctrl-K at end is a no-op"); + free(t); + editor_free(&e); + + editor f; + editor_init(&f); + type_str(&f, "foo bar"); + apply_key(&f, KEY_HOME); + apply_key(&f, KEY_RIGHT); + apply_key(&f, KEY_RIGHT); + apply_key(&f, KEY_RIGHT); + apply_key(&f, KEY_CTRL_K); + char *u = editor_text(&f); + CHECK_STR(u, "foo", "Ctrl-K cut the rest of the line"); + free(u); + editor_free(&f); + + editor g; + editor_init(&g); + type_str(&g, "foo bar"); + apply_key(&g, KEY_END); + apply_key(&g, KEY_LEFT); + apply_key(&g, KEY_LEFT); + apply_key(&g, KEY_LEFT); + apply_key(&g, KEY_CTRL_U); + char *v = editor_text(&g); + CHECK_STR(v, "bar", "Ctrl-U cut the front of the line"); + free(v); + editor_free(&g); + + editor h; + editor_init(&h); + type_str(&h, "hello bar_baz"); + apply_key(&h, KEY_END); + apply_key(&h, KEY_CTRL_W); + char *w = editor_text(&h); + CHECK_STR(w, "hello", "Ctrl-W removed one word"); + free(w); + editor_free(&h); +} + +static void test_editor_undo(void) { + editor e; + editor_init(&e); + type_str(&e, "hello"); + char *t = editor_text(&e); + CHECK_STR(t, "hello", "typed"); + free(t); + + apply_key(&e, KEY_UNDO); + t = editor_text(&e); + CHECK_STR(t, "hell", "undo removed the last char"); + free(t); + + apply_key(&e, KEY_UNDO); + t = editor_text(&e); + CHECK_STR(t, "hel", "undo again"); + free(t); + + apply_key(&e, KEY_UNDO); + apply_key(&e, KEY_UNDO); + apply_key(&e, KEY_UNDO); + t = editor_text(&e); + CHECK_STR(t, "", "undo back to empty"); + free(t); + + apply_key(&e, KEY_UNDO); + t = editor_text(&e); + CHECK_STR(t, "", "undo past empty is a no-op"); + free(t); + editor_free(&e); +} + +static void test_editor_yank(void) { + /* Ctrl-W kill, then Ctrl-Y yank it back. */ + editor e; + editor_init(&e); + type_str(&e, "foo bar"); + apply_key(&e, KEY_END); + apply_key(&e, KEY_CTRL_W); + char *t = editor_text(&e); + CHECK_STR(t, "foo", "Ctrl-W killed the last word"); + free(t); + apply_key(&e, KEY_CTRL_Y); + t = editor_text(&e); + CHECK_STR(t, "foo bar", "Ctrl-Y yanked the killed word"); + free(t); + editor_free(&e); + + /* A backspaced char is also yankable. */ + editor f; + editor_init(&f); + type_str(&f, "ab"); + apply_key(&f, KEY_BACKSPACE); + char *u = editor_text(&f); + CHECK_STR(u, "a", "backspace removed b"); + free(u); + apply_key(&f, KEY_CTRL_Y); + u = editor_text(&f); + CHECK_STR(u, "ab", "Ctrl-Y yanked the backspaced char"); + free(u); + editor_free(&f); +} + +static void test_editor_ctrl_c_and_ctrl_d(void) { + editor e; + editor_init(&e); + type_str(&e, "hello"); + input_action a; + editor_apply(&e, kkind(KEY_CTRL_C), &a); + CHECK(a.kind == ACT_NONE, "Ctrl-C on non-empty is an edit op"); + CHECK(editor_is_empty(&e), "Ctrl-C clears the buffer"); + input_action_free(&a); + + input_action q; + editor_apply(&e, kkind(KEY_CTRL_C), &q); + CHECK(q.kind == ACT_QUIT, "Ctrl-C on empty -> quit"); + input_action_free(&q); + editor_free(&e); + + editor f; + editor_init(&f); + input_action d; + editor_apply(&f, kkind(KEY_CTRL_D), &d); + CHECK(d.kind == ACT_QUIT, "Ctrl-D on empty -> quit"); + input_action_free(&d); + + type_str(&f, "hello"); + input_action d2; + editor_apply(&f, kkind(KEY_CTRL_D), &d2); + CHECK(d2.kind == ACT_NONE, "Ctrl-D on non-empty is an edit op"); + char *t = editor_text(&f); + CHECK_STR(t, "hello", "no-op at end of a single line"); + free(t); + input_action_free(&d2); + editor_free(&f); +} + +static void test_editor_backtab_tab_and_esc(void) { + editor e; + editor_init(&e); + input_action a; + editor_apply(&e, kkind(KEY_BACKTAB), &a); + CHECK(a.kind == ACT_TOGGLE_MODE, "backtab toggles mode"); + input_action_free(&a); + editor_free(&e); + + editor f; + editor_init(&f); + input_action t; + editor_apply(&f, kkind(KEY_TAB), &t); + CHECK(t.kind == ACT_NONE, "tab is an insert op"); + char *ts = editor_text(&f); + CHECK_STR(ts, "\t", "tab inserted"); + free(ts); + input_action_free(&t); + editor_free(&f); + + editor g; + editor_init(&g); + input_action s; + editor_apply(&g, kkind(KEY_ESC), &s); + CHECK(s.kind == ACT_STOP, "esc -> stop"); + input_action_free(&s); + editor_free(&g); +} + +/* ================================================================== */ +/* Display tests */ +/* ================================================================== */ + +static void test_display_len(void) { + CHECK(input_display_len("\x1b[1;36m›\x1b[0m ") == 2, "SGR ignored"); + CHECK(input_display_len("plain") == 5, "plain ascii"); + CHECK(input_display_len("") == 0, "empty"); +} + +static void test_display_len_wide(void) { + CHECK(input_display_len("\xe4\xb8\xad\xe6\x96\x87") == 4, "two CJK = 4"); + CHECK(input_display_len("a\xe4\xb8\xad" "b") == 4, "a CJK b = 4"); + CHECK(input_display_len("h\xc3\xa9llo") == 5, "accented = 5"); +} + +static void test_display_len_escape_families(void) { + CHECK(input_display_len("\x1b]0;llsh\x1b\\hi") == 2, "OSC st"); + CHECK(input_display_len("\x1b]0;llsh\x07hi") == 2, "OSC BEL"); + CHECK(input_display_len("\x1bOA") == 0, "SS3"); + CHECK(input_display_len("\x1b(Bx") == 1, "charset"); + CHECK(input_display_len("\x1b[c") == 0, "CSI cursor"); + CHECK(input_display_len("a\x1b") == 1, "trailing lone ESC"); +} + +/* History tests */ +/* ================================================================== */ + +static char *hist_dir(void) { + static char buf[512]; + snprintf(buf, sizeof(buf), "/tmp/llshc_hist_%d", (int)getpid()); + return buf; +} + +static char *hist_path(const char *name) { + static char path[640]; + snprintf(path, sizeof(path), "%s/%s", hist_dir(), name); + return path; +} + +static void hist_cleanup(void) { + const char *names[] = {"a.json", "b.json"}; + for (size_t i = 0; i < 2; i++) { + char p[640]; + snprintf(p, sizeof(p), "%s/%s", hist_dir(), names[i]); + remove(p); + } + rmdir(hist_dir()); +} + +static void test_history_capture_submit_cycle(void) { + hist_cleanup(); + char *path = hist_path("a.json"); + + input_history *h = input_history_open(path); + CHECK(h != NULL, "open history"); + input_history_push(h, "echo hi"); + input_history_push(h, "ls -la"); + + /* While composing, Up engages only from the first line (caret_line 0); + * a mid-buffer Up moves the caret, not history. */ + CHECK(input_history_walk(h, -1, 1, "") == NULL, "Up mid-buffer -> no recall"); + + char *line = input_history_walk(h, -1, 0, ""); + CHECK_STR(line, "ls -la", "newest first"); + free(line); + + /* Mid-walk: Up advances toward the oldest regardless of the caret line. */ + line = input_history_walk(h, -1, 2, ""); + CHECK_STR(line, "echo hi", "oldest second"); + free(line); + + /* Oldest already shown: Up hands back to the caret. */ + CHECK(input_history_walk(h, -1, 0, "") == NULL, "Up at oldest -> NULL"); + + /* Down walks back toward the newest entry. */ + line = input_history_walk(h, +1, 0, ""); + CHECK_STR(line, "ls -la", "Down back to newest"); + free(line); + + /* Down past the newest restores the pre-recall line and ends the walk. */ + line = input_history_walk(h, +1, 0, ""); + CHECK_STR(line, "", "Down past newest restores pre-recall line"); + free(line); + CHECK(h->active == false, "deactivated after restoring"); + + input_history_free(h); + + input_history *h2 = input_history_open(path); + CHECK(h2 && h2->entries.len == 2, "reloaded 2 entries"); + input_history_free(h2); + hist_cleanup(); +} + +static void test_history_restores_pre_recall_line(void) { + hist_cleanup(); + char *path = hist_path("a.json"); + input_history *h = input_history_open(path); + input_history_push(h, "first"); + input_history_push(h, "second"); + + /* Down while composing never recalls — it moves the caret. */ + CHECK(input_history_walk(h, +1, 0, "typed but not sent") == NULL, + "Down while composing -> no recall"); + + char *line = input_history_walk(h, -1, 0, "typed but not sent"); + CHECK_STR(line, "second", "newest first"); + free(line); + + line = input_history_walk(h, +1, 0, "typed but not sent"); + CHECK_STR(line, "typed but not sent", "restored the pre-recall line"); + free(line); + CHECK(h->active == false, "deactivated after restoring"); + CHECK(h->snapshot == NULL, "snapshot consumed"); + input_history_free(h); + hist_cleanup(); +} + +static void test_history_skips_duplicates_and_empty(void) { + hist_cleanup(); + char *path = hist_path("a.json"); + input_history *h = input_history_open(path); + input_history_push(h, "same"); + input_history_push(h, "same"); + input_history_push(h, " "); + input_history_push(h, " spaced "); + input_history_push(h, "spaced"); + + CHECK(h->entries.len == 2, "two unique entries"); + if (h->entries.len == 2) { + CHECK_STR(h->entries.items[0], "same", "entry 0"); + CHECK_STR(h->entries.items[1], "spaced", "entry 1 trimmed"); + } + input_history_free(h); + hist_cleanup(); +} + +static void test_history_persistence_and_limit(void) { + hist_cleanup(); + char *path = hist_path("b.json"); + + input_history *h = input_history_open(path); + for (int i = 1; i <= LLSH_HISTORY_LIMIT + 1; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "cmd%03d", i); + input_history_push(h, buf); + } + size_t live = h->entries.len - h->head; + CHECK(live == LLSH_HISTORY_LIMIT, "limit enforced"); + if (live == LLSH_HISTORY_LIMIT) { + CHECK_STR(h->entries.items[h->head], "cmd002", "oldest evicted"); + CHECK_STR(h->entries.items[h->entries.len - 1], "cmd101", + "newest preserved"); + } + input_history_free(h); + + input_history *h2 = input_history_open(path); + CHECK(h2 && h2->entries.len - h2->head == LLSH_HISTORY_LIMIT, + "reloaded at limit"); + input_history_free(h2); + hist_cleanup(); +} + +/* ================================================================== */ +/* Shell shadow tests */ +/* ================================================================== */ + +static void test_shell_shadow(void) { + hist_cleanup(); + editor shadow; + editor_init(&shadow); + type_str(&shadow, "echo hi"); + char *out = shell_shadow_apply(&shadow, kkind(KEY_ENTER)); + CHECK_STR(out, "echo hi", "submitted line returned"); + free(out); + CHECK(editor_is_empty(&shadow), "shadow cleared after submit"); + editor_free(&shadow); + + editor s2; + editor_init(&s2); + type_str(&s2, "ls"); + char *o2 = shell_shadow_apply(&s2, kkind(KEY_LEFT)); + CHECK(o2 == NULL, "intermediate key -> NULL"); + CHECK(!editor_is_empty(&s2), "shadow NOT cleared on intermediate key"); + editor_free(&s2); + + editor s3; + editor_init(&s3); + type_str(&s3, " "); + char *o3 = shell_shadow_apply(&s3, kkind(KEY_ENTER)); + CHECK(o3 == NULL, "blank submit -> NULL"); + CHECK(!editor_is_empty(&s3), "shadow NOT cleared on blank"); + editor_free(&s3); + hist_cleanup(); +} + +/* ================================================================== */ +/* Reader queue */ +/* ================================================================== */ + +/* The reader's event queue must be FIFO: keys typed quickly must reach the + * editor in order (regression: a swap-pop scrambled the sequence, dropping + * and reordering characters). */ +static void test_reader_queue_fifo(void) { + reader_ctx *ctx = (reader_ctx* ) xmalloc(sizeof *ctx); + memset(ctx, 0, sizeof *ctx); + input_key_reader *r = input_key_reader_new(ctx); + + const unsigned int cps[] = {'a', 'b', 'c', 'd', 'e', 'f'}; + for (size_t i = 0; i < sizeof cps / sizeof *cps; i++) { + input_key k = {KEY_CHAR, cps[i]}; + input_key_reader_send(r, &k); + } + for (size_t i = 0; i < sizeof cps / sizeof *cps; i++) { + input_key out; + const unsigned char *raw; + size_t raw_len; + CHECK(input_key_reader_recv(r, &out, &raw, &raw_len), + "recv returns queued event"); + CHECK(out.kind == KEY_CHAR, "queued key kind preserved"); + CHECK(out.cp == cps[i], "queue is FIFO (order preserved)"); + input_raw_free(raw); + } + input_key out; + CHECK(!input_key_reader_recv(r, &out, NULL, NULL), "queue drained"); + input_key_reader_free(r); +} + +/* The raw buffer handed out by recv is owned by the caller: it must stay + * valid no matter what the queue does afterwards (regression: recv used to + * pop-and-free the very buffer it returned, so callers read freed memory and + * shell-mode keystrokes arrived scrambled). */ +static void test_recv_raw_owned(void) { + reader_ctx *ctx = (reader_ctx* ) xmalloc(sizeof *ctx); + memset(ctx, 0, sizeof *ctx); + input_key_reader *r = input_key_reader_new(ctx); + + input_key k = {KEY_CHAR, 'x'}; + input_key_reader_send_raw(r, &k, "payload"); + + input_key out; + const unsigned char *raw1 = NULL; + size_t raw_len = 0; + CHECK(input_key_reader_recv(r, &out, &raw1, &raw_len), "recv raw event"); + CHECK(raw_len == 7, "raw length preserved"); + + /* Fill the ring past one full lap so the queue reuses the first slot; + * with a borrowed (freed-on-pop) pointer, `raw1` would now be garbage. */ + for (int i = 0; i < 16; i++) + input_key_reader_send_raw(r, &k, "scratch"); + for (int i = 0; i < 16; i++) { + input_key o2; + const unsigned char *r2 = NULL; + if (!input_key_reader_recv(r, &o2, &r2, &raw_len)) + break; + input_raw_free(r2); + } + + CHECK(raw1 && raw1[0] == 'p' && memcmp(raw1, "payload", 7) == 0, + "recv'd raw stays valid after the queue laps it"); + input_raw_free(raw1); + input_key_reader_free(r); +} + +/* ================================================================== */ + +/* The renderer and these tests share input_wrap_total / input_wrap_caret to + * agree on how a buffer breaks into physical rows and where the caret lands on + * a terminal of a given width, using the delayed-wrap model real terminals use. + * Line 0 carries the 2-column prompt. */ +static void test_wrap_geometry(void) { + size_t r, c; + + /* A short first line: the prompt takes the first two columns, nothing + * wraps. */ + CHECK(input_wrap_total("hi", 10) == 1, "short line is one row"); + input_wrap_caret("hi", 0, 10, &r, &c); + CHECK(r == 0 && c == 2, "caret at text start sits after the prompt"); + input_wrap_caret("hi", 2, 10, &r, &c); + CHECK(r == 0 && c == 4, "caret at the text end"); + + /* A first line long enough to wrap: prompt + text flow across rows. The + * cursor sits on the last typed column, not a spurious next row. */ + CHECK(input_wrap_total("abcdef", 5) == 2, "wrapped line is two rows"); + input_wrap_caret("abcdef", 0, 5, &r, &c); + CHECK(r == 0 && c == 2, "caret before the first char"); + input_wrap_caret("abcdef", 3, 5, &r, &c); + CHECK(r == 0 && c == 4, "caret after 'abc' sits on the last column"); + input_wrap_caret("abcdef", 6, 5, &r, &c); + CHECK(r == 1 && c == 3, "caret at the wrapped end"); + + /* A multi-line buffer: only line 0 carries the prompt. */ + CHECK(input_wrap_total("ab\ncdef", 5) == 2, "two short lines are two rows"); + input_wrap_caret("ab\ncdef", 7, 5, &r, &c); + CHECK(r == 1 && c == 4, "caret at the buffer end"); + input_wrap_caret("ab\ncdef", 5, 5, &r, &c); + CHECK(r == 1 && c == 2, "caret mid second line"); + + /* Regression: a first line that ends exactly at the margin, followed by a + * newline, must NOT gain a spurious empty row. '» ab' is exactly 4 + * columns, so the 'cdefgh' line starts immediately on row 1. */ + CHECK(input_wrap_total("ab\ncdefgh", 4) == 3, + "full first line + newline is three rows, not four"); + input_wrap_caret("ab\ncdefgh", 9, 4, &r, &c); + CHECK(r == 2 && c == 2, "caret at the end of the third row"); + input_wrap_caret("ab\ncdefgh", 2, 4, &r, &c); + CHECK(r == 0 && c == 3, "caret at the end of the full first row"); + input_wrap_caret("ab\ncdefgh", 3, 4, &r, &c); + CHECK(r == 1 && c == 0, "caret at the start of the second line"); + + /* A trailing full line keeps the cursor on its last column (no empty row). */ + CHECK(input_wrap_total("abcdef", 4) == 2, "two full rows is two rows"); + input_wrap_caret("abcdef", 6, 4, &r, &c); + CHECK(r == 1 && c == 3, "caret at the end of a full row sits on it"); + + /* The renderer needs the pending flag to tell a cursor sitting on the last + * column (delayed wrap) from one merely after the second-to-last column. */ + CHECK(input_wrap_pending("hi", 2, 10) == false, + "short end is not pending"); + CHECK(input_wrap_pending("abcdef", 3, 5) == true, + "caret after the last-column char is pending"); + CHECK(input_wrap_pending("abcdef", 4, 5) == false, + "caret on the wrapped row is not pending"); + CHECK(input_wrap_pending("abcdefg", 7, 10) == false, + "second-to-last column end is not pending"); + CHECK(input_wrap_pending("abcdefgh", 8, 10) == true, + "last-column end is pending"); + CHECK(input_wrap_pending("a\xE4\xB8\xAD", 4, 10) == false, + "three-byte char advances one column"); + CHECK(input_wrap_pending("a\xF0\x9F\x98\x80", 5, 10) == false, + "four-byte char advances one column"); + CHECK(input_wrap_pending("a", 1, 1) == true, + "one-column terminal stays pending"); + CHECK(input_wrap_pending("a\n", 2, 1) == false, + "newline clears pending wrap"); + CHECK(input_wrap_pending("ab\ncdef\n", 8, 5) == false, + "a trailing newline is not pending"); + + /* A blank buffer still renders the prompt as one row. */ + CHECK(input_wrap_total("", 10) == 1, "blank buffer is one row"); + input_wrap_caret("", 0, 10, &r, &c); + CHECK(r == 0 && c == 2, "caret sits after the prompt"); +} + +int main(void) { + /* parser */ + test_parser_plain_chars(); + test_parser_enter_variants(); + test_parser_shift_enter_and_esc(); + test_parser_lone_escape_is_esc(); + test_parser_alt_letter_is_the_letter(); + test_parser_raw_bytes_preserved(); + test_parser_control_keys(); + test_parser_unmapped_control_bytes(); + test_parser_unknown_sequence_consumed(); + test_parser_bracketed_paste(); + test_parser_utf8(); + test_parser_mixed_stream(); + test_parser_kitty_and_ss3(); + + /* editor */ + test_editor_typing_and_submit(); + test_editor_shift_enter_inserts_newline(); + test_editor_backslash_enter_continues(); + test_editor_middle_line_continuation(); + test_ctrl_j_is_dead_key(); + test_editor_backspace_and_delete(); + test_editor_multibyte_backspace(); + test_editor_horizontal_movement(); + test_editor_vertical_movement(); + test_editor_insert_at_caret(); + test_editor_kill_and_word_kills(); + test_editor_undo(); + test_editor_yank(); + test_editor_ctrl_c_and_ctrl_d(); + test_editor_backtab_tab_and_esc(); + + /* display */ + test_display_len(); + test_display_len_wide(); + test_display_len_escape_families(); + + /* wrap geometry */ + test_wrap_geometry(); + + /* history */ + test_history_capture_submit_cycle(); + test_history_restores_pre_recall_line(); + test_history_skips_duplicates_and_empty(); + test_history_persistence_and_limit(); + + /* shell shadow */ + test_shell_shadow(); + + /* reader queue */ + test_reader_queue_fifo(); + test_recv_raw_owned(); + + printf("%d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} |