summaryrefslogtreecommitdiff
path: root/tests/resize_property_test.c
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-12 17:49:34 +0300
commit4384435941b8c7658856b381d071aee2e0035e89 (patch)
tree5ece99c3dd1decb8ec3fd404c0ece2b5865a552a /tests/resize_property_test.c
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:<name>. - 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 <noreply@anthropic.com>
Diffstat (limited to 'tests/resize_property_test.c')
-rw-r--r--tests/resize_property_test.c156
1 files changed, 156 insertions, 0 deletions
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 <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+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;
+}