From 4384435941b8c7658856b381d071aee2e0035e89 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Mon, 12 Oct 2026 17:49:34 +0300 Subject: term: move the term core to subprojects/term The core now builds, tests, and benchmarks alone, as a Meson subproject with no dependency on the main tree. - include/term/ holds term.h and the header-only unicode/. src/ holds the core and its private headers. The main tree gets only include/term through term_dep, so it cannot reach a private header. - The term tests, the term benchmarks, the Kitty parser suite, and tools/amalgamate.py and tools/generate_unicode.py move with it. The tests run as term:. - The main build passes its -march (arch_flag) and b_ndebug to the subproject. A directory configured before this commit needs meson configure -Dterm:arch_flag=-march=... once. - The term tests compile with -UNDEBUG. They do work inside assert. - The subproject exports term_test_dep with term_api.h, for the main tree's render, thumbnail, color, and selection tests. - The Kitty bridge used struct vector2i and has not compiled since the public API redesign. It builds again. 2 of its 14 tests fail: REP after a tab, and a cursor report after a resize. - dirgraph.py drops the term and unicode layers. Its self-test uses pty. - The symbols test ignores the __odr_asan symbols that ASan adds. Co-Authored-By: Claude Opus 5.5 --- tests/resize_property_test.c | 156 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/resize_property_test.c (limited to 'tests/resize_property_test.c') diff --git a/tests/resize_property_test.c b/tests/resize_property_test.c new file mode 100644 index 0000000..360e640 --- /dev/null +++ b/tests/resize_property_test.c @@ -0,0 +1,156 @@ +#include "term/term.h" +#include "term_api.h" + +#include +#include +#include +#include + +static uint64_t state; + +static unsigned random_below(unsigned limit) +{ + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + return (unsigned)(state % limit); +} + +/* Select from the oldest retained row to the end of the live screen. + * Selection text joins wrapped rows and trims blank cells on other rows. */ +static char *logical_text(struct term *t) +{ + struct term_vector2i size = term_size(t); + term_scroll_to(t, term_scrollback(t).total); + term_select_start(t, (struct term_vector2i){ .x = 0, .y = 0 }, false, TERM_SELECT_CELL); + term_scroll_to(t, 0); + term_select_extend(t, (struct term_vector2i){ .x = size.x - 1, .y = size.y - 1 }, true); + char *text = tt_selection_text(t); + term_select_clear(t); + if (!text) return NULL; + size_t out = 0; + for (size_t i = 0; text[i]; i++) { + if (text[i] == '\n') { + while (out && text[out - 1] == ' ') out--; + } + text[out++] = text[i]; + } + while (out && (text[out - 1] == '\n' || text[out - 1] == ' ')) out--; + text[out] = 0; + return text; +} + +static uint32_t cell_at(struct term *t, struct term_snapshot *s, int x, int y) +{ + if (!term_snapshot_update(s, t)) return 0; + return term_snapshot_row(s, y)[x].text; +} + +/* A height change that pulls rows back from history moves the saved + * cursor with its row. DECRC then prints next to the saved text. */ +static int saved_cursor_follows_history(struct term_snapshot *s) +{ + static const struct { int cols, rows, grow_cols, grow_rows; } cases[] = { + { 8, 3, 8, 5 }, /* same width: rows come back from history */ + { 8, 3, 10, 5 }, /* new width: reflow */ + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + struct term_options opt = { + .size = { .x = cases[i].cols, .y = cases[i].rows }, .scrollback = 10, + }; + struct term *t = term_new(&opt); + if (!t) return 1; + const char *text = "l1\r\nl2\r\nl3\r\nl4\r\nab\0337\033[1;1H"; + term_feed(t, text, strlen(text), 0); + struct term_vector2i size = { .x = cases[i].grow_cols, .y = cases[i].grow_rows }; + if (!term_resize(t, size)) return 1; + term_feed(t, "\0338X", 3, 0); + struct term_cursor c = term_cursor(t); + int y = c.pos.y; + if (cell_at(t, s, 0, y) != 'a' || cell_at(t, s, 1, y) != 'b' || + cell_at(t, s, 2, y) != 'X') { + fprintf(stderr, "case %zu: DECRC after a resize lost the saved row\n", i); + return 1; + } + term_free(t); + } + return 0; +} + +/* Narrowing a full screen with the cursor at the top wraps more rows than + * the ring holds. The cursor row must survive and stay on screen. */ +static int narrow_resize_keeps_cursor_row(struct term_snapshot *s) +{ + struct term_options opt = { .size = { .x = 40, .y = 12 }, .scrollback = 5 }; + struct term *t = term_new(&opt); + if (!t) return 1; + for (int y = 0; y < 12; y++) { + char line[64]; + memset(line, 'a' + y, 40); + line[40] = 0; + term_feed(t, line, 40, 0); + } + term_feed(t, "\033[1;1H", 6, 0); + if (!term_resize(t, (struct term_vector2i){ .x = 1, .y = 12 })) return 1; + struct term_cursor c = term_cursor(t); + if (c.pos.x != 0 || c.pos.y != 0 || cell_at(t, s, 0, 0) != 'a') { + fprintf(stderr, "narrow resize: cursor %d,%d, cell %x\n", c.pos.x, c.pos.y, + cell_at(t, s, 0, 0)); + return 1; + } + term_feed(t, "Z", 1, 0); + if (cell_at(t, s, 0, 0) != 'Z') return 1; + term_free(t); + return 0; +} + +int main(int argc, char **argv) +{ + struct term_snapshot *snap = term_snapshot_new(); + if (!snap || saved_cursor_follows_history(snap) || narrow_resize_keeps_cursor_row(snap)) + return 1; + term_snapshot_free(snap); + + int seeds = argc > 1 ? atoi(argv[1]) : 100; + static const char *word[] = { + "alpha", " x", "中", "日", "e\xcc\x81", "🦊", "👍", " beta", "123", "a", + }; + for (int seed = 1; seed <= seeds; seed++) { + state = 0x9e3779b97f4a7c15ull * (uint64_t)seed; + struct term_options opt = { + .size = { .x = 40, .y = 12 }, + .scrollback = 4096, + }; + struct term *t = term_new(&opt); + if (!t) return 1; + for (int line = 0; line < 16; line++) { + int words = 1 + (int)random_below(4); + for (int i = 0; i < words; i++) { + const char *s = word[random_below(sizeof(word) / sizeof(word[0]))]; + term_feed(t, s, strlen(s), 0); + } + if (line != 15) term_feed(t, "\r\n", 2, 0); + } + char *before = logical_text(t); + if (!before) return 1; + for (int step = 0; step < 20; step++) { + struct term_vector2i size = { + .x = 1 + (int)random_below(200), + .y = 1 + (int)random_below(60), + }; + if (!term_resize(t, size)) return 1; + char *after = logical_text(t); + if (!after) return 1; + if (strcmp(before, after)) { + fprintf(stderr, "seed %d step %d resize %dx%d: logical text changed\n" + "before: <%s>\nafter: <%s>\n", + seed, step, size.x, size.y, before, after); + return 1; + } + free(after); + } + free(before); + term_free(t); + } + return 0; +} -- cgit v1.2.3