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/hist_test.c | 765 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 765 insertions(+) create mode 100644 tests/hist_test.c (limited to 'tests/hist_test.c') diff --git a/tests/hist_test.c b/tests/hist_test.c new file mode 100644 index 0000000..be9fd3f --- /dev/null +++ b/tests/hist_test.c @@ -0,0 +1,765 @@ +#include "hist.h" +#include "hist_view.h" +#include "term/unicode/grapheme.h" + +#include +#include +#include +#include +#include + +static uint64_t lines(const struct hist *h) +{ + return hist_end_id(h) - hist_first_id(h); +} + +static struct cell dc(uint32_t c) +{ + return (struct cell){ .c = c, .fg = TERM_COLOR_FG, .bg = TERM_COLOR_BG }; +} + +static void test_init_free(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + assert(lines(&h) == 0); + assert(hist_first_id(&h) == 0); + assert(!hist_is_open(&h)); + term_hist_free(&h); +} + +static void test_ascii_roundtrip(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + + const char *msg = "Hello, world!"; + term_hist_push_ascii(&h, msg, (int)strlen(msg), NULL, false); + assert(lines(&h) == 1); + assert(term_hist_line_rows(&h, 0, 80) == 1); + assert(term_hist_line_rows(&h, 0, 5) == 3); + + struct cell out[80]; + int n = term_hist_decode_segment(&h, 0, 0, 80, out, NULL); + assert(n == (int)strlen(msg)); + for (int i = 0; i < n; i++) { + assert(out[i].c == (uint32_t)msg[i]); + assert(out[i].fg == TERM_COLOR_FG); + assert(out[i].bg == TERM_COLOR_BG); + assert(out[i].flags == 0); + } + + assert(term_hist_decode_segment(&h, 0, 0, 5, out, NULL) == 5); + assert(out[0].c == 'H' && out[4].c == 'o'); + assert(term_hist_decode_segment(&h, 0, 1, 5, out, NULL) == 5); + assert(out[0].c == ',' && out[4].c == 'r'); + assert(term_hist_decode_segment(&h, 0, 2, 5, out, NULL) == 3); + assert(out[0].c == 'l' && out[2].c == '!'); + + term_hist_free(&h); +} + +static void test_attributes_roundtrip(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + + struct cell row[10]; + for (int i = 0; i < 10; i++) { + row[i] = (struct cell){ + .c = 'A' + i, + .fg = term_color_rgb(i * 10, i * 20, i * 25), + .bg = term_color_rgb(255 - i * 10, 0, 0), + .flags = (i % 2 == 0) ? CELL_BOLD : CELL_ITALIC, + .pad = (uint16_t)(100 + i), + }; + } + term_hist_push_row(&h, row, 10, false, NULL); + assert(lines(&h) == 1); + + struct cell out[10]; + assert(term_hist_decode_segment(&h, 0, 0, 10, out, NULL) == 10); + for (int i = 0; i < 10; i++) { + assert(out[i].c == row[i].c); + assert(out[i].fg == row[i].fg); + assert(out[i].bg == row[i].bg); + assert(out[i].flags == row[i].flags); + assert(out[i].pad == row[i].pad); + } + term_hist_free(&h); +} + +static void test_wide_and_wrap(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + + struct cell row[4] = { dc('A'), dc(0x4e16), dc(' '), dc('B') }; + row[1].flags = CELL_WIDE; + row[2].flags = CELL_TRAIL; + term_hist_push_row(&h, row, 4, false, NULL); + + assert(term_hist_line_rows(&h, 0, 4) == 1); + /* Width 2: 'A' and a pad, then the wide cell, then 'B'. */ + assert(term_hist_line_rows(&h, 0, 2) == 3); + + struct cell out[4]; + assert(term_hist_decode_segment(&h, 0, 0, 2, out, NULL) == 2); + assert(out[0].c == 'A'); + assert(out[1].flags & CELL_WRAP_PAD); + assert(term_hist_decode_segment(&h, 0, 1, 2, out, NULL) == 2); + assert(out[0].c == 0x4e16 && (out[0].flags & CELL_WIDE)); + assert(out[1].flags & CELL_TRAIL); + assert(term_hist_decode_segment(&h, 0, 2, 2, out, NULL) == 1); + assert(out[0].c == 'B'); + term_hist_free(&h); +} + +static void test_clusters_roundtrip(void) +{ + struct gstore *g = term_gstore_new(NULL); + assert(g); + uint32_t cps[3] = { 'e', 0x0301, 0x0300 }; + uint32_t handle = term_gstore_intern(g, cps, 3); + + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + struct cell row[2] = { dc(handle), dc('X') }; + term_hist_push_row(&h, row, 2, false, g); + + struct cell out[2]; + assert(term_hist_decode_segment(&h, 0, 0, 2, out, g) == 2); + assert(grapheme_is_handle(out[0].c)); + uint32_t got[GRAPHEME_MAX_LEN]; + assert(term_gstore_expand(g, out[0].c, got, GRAPHEME_MAX_LEN) == 3); + assert(got[0] == cps[0] && got[1] == cps[1] && got[2] == cps[2]); + assert(out[1].c == 'X'); + + term_hist_free(&h); + term_gstore_free(g); +} + +static void test_open_lines_and_pop(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + + struct cell part1[3] = { dc('1'), dc('2'), dc('3') }; + term_hist_push_row(&h, part1, 3, true, NULL); + assert(lines(&h) == 1 && hist_is_open(&h)); + struct cell part2[2] = { dc('4'), dc('5') }; + term_hist_push_row(&h, part2, 2, false, NULL); + assert(lines(&h) == 1 && !hist_is_open(&h)); + + struct cell out[5]; + assert(term_hist_decode_segment(&h, 0, 0, 10, out, NULL) == 5); + for (int i = 0; i < 5; i++) + assert(out[i].c == (uint32_t)('1' + i)); + + struct cell popped[2]; + assert(term_hist_pop_tail_cells(&h, 2, popped, NULL) == 2); + assert(popped[0].c == '4' && popped[1].c == '5'); + assert(term_hist_decode_segment(&h, 0, 0, 10, out, NULL) == 3); + assert(out[0].c == '1' && out[2].c == '3'); + + /* Pop from an open line with a pen. */ + term_hist_close_open_line(&h); + struct cell color[2] = { + { .c = 'A', .fg = 0xff0000, .bg = 0x00ff00, .flags = CELL_BOLD }, + { .c = 'B', .fg = 0xff0000, .bg = 0x00ff00, .flags = CELL_BOLD }, + }; + term_hist_push_row(&h, color, 2, true, NULL); + assert(lines(&h) == 2); + struct cell pop_color[1]; + assert(term_hist_pop_tail_cells(&h, 1, pop_color, NULL) == 1); + assert(pop_color[0].c == 'B' && pop_color[0].fg == 0xff0000 && pop_color[0].bg == 0x00ff00); + assert(pop_color[0].flags & CELL_BOLD); + assert(term_hist_decode_segment(&h, 1, 0, 10, out, NULL) == 1); + assert(out[0].c == 'A' && out[0].fg == 0xff0000 && (out[0].flags & CELL_BOLD)); + + /* Popping a whole line returns its cells and removes it. */ + struct cell all[4]; + assert(term_hist_pop_line(&h, all, 4, NULL) == 1); + assert(all[0].c == 'A'); + assert(lines(&h) == 1 && !hist_is_open(&h)); + term_hist_free(&h); +} + +static void test_open_line_joins(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + + struct cell line1[2] = { dc('X'), dc('Y') }; + term_hist_push_row(&h, line1, 2, true, NULL); + term_hist_close_open_line(&h); + struct cell line2[2] = { dc('Z'), dc('W') }; + term_hist_push_row(&h, line2, 2, false, NULL); + assert(lines(&h) == 2); + + struct cell out[16]; + assert(term_hist_decode_segment(&h, 0, 0, 10, out, NULL) == 2); + assert(out[0].c == 'X' && out[1].c == 'Y'); + assert(term_hist_decode_segment(&h, 1, 0, 10, out, NULL) == 2); + assert(out[0].c == 'Z' && out[1].c == 'W'); + + /* ASCII joins a wide open line. */ + struct cell wide[2] = { { .c = 0x4e2d, .fg = 0x123456, .bg = TERM_COLOR_BG, .flags = CELL_WIDE }, + { .c = 0, .fg = 0x123456, .bg = TERM_COLOR_BG, .flags = CELL_TRAIL } }; + term_hist_push_row(&h, wide, 2, true, NULL); + term_hist_push_ascii(&h, "hello", 5, NULL, false); + assert(lines(&h) == 3); + assert(term_hist_decode_segment(&h, 2, 0, 10, out, NULL) == 7); + assert(out[0].c == 0x4e2d && out[0].fg == 0x123456); + assert(out[2].c == 'h' && out[2].fg == TERM_COLOR_FG); + term_hist_free(&h); +} + +static void test_eviction(void) +{ + struct hist h; + assert(term_hist_init(&h, 5, 64 * 1024 * 2)); + for (int i = 0; i < 10; i++) { + char buf[16]; + snprintf(buf, sizeof(buf), "line%d", i); + term_hist_push_ascii(&h, buf, (int)strlen(buf), NULL, false); + } + assert(lines(&h) == 5); + assert(hist_first_id(&h) == 5 && hist_end_id(&h) == 10); + + struct cell out[16]; + assert(term_hist_decode_segment(&h, 5, 0, 16, out, NULL) == 5); + assert(out[0].c == 'l' && out[4].c == '5'); + assert(term_hist_decode_segment(&h, 9, 0, 16, out, NULL) == 5); + assert(out[4].c == '9'); + assert(term_hist_decode_segment(&h, 4, 0, 16, out, NULL) == 0); + term_hist_free(&h); +} + +static void test_emoji_and_text_presentation(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + struct cell row[3] = { dc(0x2764), dc(' '), dc(0x2600) }; + row[0].flags = CELL_WIDE | CELL_EMOJI; + row[1].flags = CELL_TRAIL; + row[2].flags = CELL_TEXT; + term_hist_push_row(&h, row, 3, false, NULL); + + struct cell out[3]; + assert(term_hist_decode_segment(&h, 0, 0, 10, out, NULL) == 3); + assert(out[0].c == 0x2764 && (out[0].flags & CELL_WIDE) && (out[0].flags & CELL_EMOJI)); + assert(out[1].flags & CELL_TRAIL); + assert(out[2].c == 0x2600 && (out[2].flags & CELL_TEXT)); + term_hist_free(&h); +} + +static void test_width_one(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + term_hist_push_ascii(&h, "ABC", 3, NULL, false); + assert(term_hist_line_rows(&h, 0, 1) == 3); + + struct cell out[2]; + assert(term_hist_decode_segment(&h, 0, 0, 1, out, NULL) == 1 && out[0].c == 'A'); + assert(term_hist_decode_segment(&h, 0, 1, 1, out, NULL) == 1 && out[0].c == 'B'); + assert(term_hist_decode_segment(&h, 0, 2, 1, out, NULL) == 1 && out[0].c == 'C'); + + struct cell wide[2] = { dc(0x4e16), dc(' ') }; + wide[0].flags = CELL_WIDE; + wide[1].flags = CELL_TRAIL; + term_hist_push_row(&h, wide, 2, false, NULL); + assert(term_hist_line_rows(&h, 1, 1) == 1); + assert(term_hist_decode_segment(&h, 1, 0, 1, out, NULL) == 1); + assert(out[0].c == 0x4e16 && (out[0].flags & CELL_WIDE)); + term_hist_free(&h); +} + +static void test_blank_lines(void) +{ + struct hist h; + assert(term_hist_init(&h, 10, 64 * 1024 * 4)); + const char *first = "First line on page 0"; + term_hist_push_ascii(&h, first, (int)strlen(first), NULL, false); + term_hist_push_blank(&h, 10); + assert(lines(&h) == 10); + assert(hist_first_id(&h) == 1); + + for (int i = 0; i < 20; i++) { + char buf[32]; + snprintf(buf, sizeof(buf), "line %d", i); + term_hist_push_ascii(&h, buf, (int)strlen(buf), NULL, false); + } + struct cell out[32]; + assert(term_hist_decode_segment(&h, hist_end_id(&h) - 1, 0, 32, out, NULL) == 7); + assert(out[5].c == '1' && out[6].c == '9'); + + /* A blank row closes an open line and adds no line. */ + term_hist_push_ascii(&h, "open", 4, NULL, true); + uint64_t before = hist_end_id(&h); + term_hist_push_blank(&h, 1); + assert(!hist_is_open(&h) && hist_end_id(&h) == before); + + /* More blank rows than the limit keep the id count going. */ + term_hist_push_blank(&h, 1000); + assert(lines(&h) == 10 && hist_end_id(&h) == before + 1000); + term_hist_push_blank(&h, 5); + assert(lines(&h) == 10 && hist_end_id(&h) == before + 1005); + assert(term_hist_decode_segment(&h, hist_end_id(&h) - 1, 0, 32, out, NULL) == 0); + assert(term_hist_line_rows(&h, hist_end_id(&h) - 1, 32) == 1); + term_hist_free(&h); +} + +static void test_open_line_attrs_continuation(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + struct cell r1[5], r2[10]; + for (int i = 0; i < 5; i++) + r1[i] = (struct cell){ .c = '1' + i, .fg = term_color_rgb(255, 0, 0), .bg = TERM_COLOR_BG }; + for (int i = 0; i < 10; i++) + r2[i] = (struct cell){ .c = 'a' + i, .fg = i < 5 ? term_color_rgb(255, 0, 0) : term_color_rgb(0, 0, 255), + .bg = TERM_COLOR_BG }; + term_hist_push_row(&h, r1, 5, true, NULL); + term_hist_push_row(&h, r2, 10, false, NULL); + assert(lines(&h) == 1); + + struct cell out[15]; + assert(term_hist_decode_segment(&h, 0, 0, 15, out, NULL) == 15); + for (int i = 0; i < 10; i++) + assert(out[i].fg == term_color_rgb(255, 0, 0)); + for (int i = 10; i < 15; i++) + assert(out[i].fg == term_color_rgb(0, 0, 255)); + term_hist_free(&h); +} + +static void mark_link(void *ud, uint16_t link) +{ + ((int *)ud)[link]++; +} + +static void test_hyperlinks(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + struct pen_attr pen = pen_attr_default(); + pen.link = 42; + term_hist_push_ascii(&h, "Link text", 9, &pen, false); + struct cell row[2] = { dc('a'), dc('b') }; + row[1].pad = 7; + row[1].flags = CELL_LINK; + term_hist_push_row(&h, row, 2, true, NULL); + + struct cell out[16]; + assert(term_hist_decode_segment(&h, 0, 0, 16, out, NULL) == 9); + for (int i = 0; i < 9; i++) + assert(out[i].pad == 42); + + int marks[64] = { 0 }; + term_hist_mark_links(&h, mark_link, marks); + assert(marks[42] && marks[7]); + term_hist_free(&h); +} + +static void test_long_lines(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + + /* A line past the part limit keeps one id. */ + char chunk[1024]; + memset(chunk, 'X', sizeof(chunk)); + for (int i = 0; i < 64; i++) + term_hist_push_ascii(&h, chunk, sizeof(chunk), NULL, true); + term_hist_push_ascii(&h, chunk, 100, NULL, false); + assert(lines(&h) == 1); + assert(term_hist_line_cells(&h, 0) == 65636); + assert(term_hist_line_rows(&h, 0, 1000) == 66); + + struct cell out[1000]; + assert(term_hist_decode_segment(&h, 0, 64, 1000, out, NULL) == 1000); + for (int i = 0; i < 1000; i++) + assert(out[i].c == 'X'); + assert(term_hist_decode_segment(&h, 0, 65, 1000, out, NULL) == 636); + + memset(chunk, 'Y', sizeof(chunk)); + for (int i = 0; i < 64; i++) + term_hist_push_ascii(&h, chunk, sizeof(chunk), NULL, true); + term_hist_push_ascii(&h, chunk, 100, NULL, false); + term_hist_push_ascii(&h, "tail", 4, NULL, false); + assert(lines(&h) == 3); + assert(term_hist_line_rows(&h, 1, 1000) == 66); + assert(term_hist_decode_segment(&h, 1, 0, 1000, out, NULL) == 1000 && out[0].c == 'Y'); + assert(term_hist_decode_segment(&h, 2, 0, 1000, out, NULL) == 4 && out[0].c == 't'); + assert(term_hist_rows(&h, 1000) == 133); + + /* One long ASCII push splits into parts but reads back whole. */ + term_hist_free(&h); + assert(term_hist_init(&h, 100, 64 * 1024 * 8)); + int big_len = 150000; + char *big = malloc((size_t)big_len); + assert(big); + for (int i = 0; i < big_len; i++) + big[i] = (char)('A' + (i % 26)); + term_hist_push_ascii(&h, big, big_len, NULL, false); + assert(lines(&h) == 1); + assert(term_hist_line_rows(&h, 0, 1000) == 150); + for (int s = 0; s < 150; s++) { + assert(term_hist_decode_segment(&h, 0, s, 1000, out, NULL) == 1000); + for (int x = 0; x < 1000; x++) + assert(out[x].c == (uint32_t)big[s * 1000 + x]); + } + free(big); + term_hist_free(&h); +} + +static void test_page_limits(void) +{ + struct hist h; + struct cell out[1000]; + + /* One page: old lines leave to make room. */ + assert(term_hist_init(&h, 10000, HIST_PAGE_SIZE)); + char line64[64]; + memset(line64, 'M', sizeof(line64)); + for (int i = 0; i < 1200; i++) + term_hist_push_ascii(&h, line64, sizeof(line64), NULL, false); + assert(lines(&h) > 0 && lines(&h) < 1200); + assert(term_hist_decode_segment(&h, hist_end_id(&h) - 1, 0, 64, out, NULL) == 64); + assert(out[0].c == 'M'); + term_hist_free(&h); + + /* A line limit of one keeps the long line as one line. */ + assert(term_hist_init(&h, 1, 64 * 1024 * 4)); + char chunk[1024]; + memset(chunk, 'Z', sizeof(chunk)); + for (int i = 0; i < 65; i++) + term_hist_push_ascii(&h, chunk, sizeof(chunk), NULL, true); + assert(lines(&h) == 1 && hist_is_open(&h)); + term_hist_push_ascii(&h, "new", 3, NULL, false); + term_hist_push_ascii(&h, "next", 4, NULL, false); + assert(lines(&h) == 1); + assert(term_hist_decode_segment(&h, hist_first_id(&h), 0, 10, out, NULL) == 4); + term_hist_free(&h); + + /* A line longer than the arena drops its head but keeps its id. */ + assert(term_hist_init(&h, 10, HIST_PAGE_SIZE)); + term_hist_push_ascii(&h, "PREFIX", 6, NULL, true); + for (int i = 0; i < 100; i++) + term_hist_push_ascii(&h, chunk, sizeof(chunk), NULL, true); + term_hist_close_open_line(&h); + assert(lines(&h) == 1 && hist_first_id(&h) == 0); + assert(term_hist_line_cells(&h, 0) > 0 && term_hist_line_cells(&h, 0) < 100 * 1024); + assert(term_hist_decode_segment(&h, 0, 0, 6, out, NULL) == 6 && out[0].c == 'Z'); + term_hist_free(&h); + + /* A small open line moves to a new page instead of splitting. */ + assert(term_hist_init(&h, 10, HIST_PAGE_SIZE)); + for (int i = 0; i < 60; i++) + term_hist_push_ascii(&h, chunk, sizeof(chunk), NULL, false); + term_hist_push_ascii(&h, "PREFIX", 6, NULL, true); + for (int i = 0; i < 10; i++) + term_hist_push_ascii(&h, chunk, sizeof(chunk), NULL, true); + term_hist_close_open_line(&h); + uint64_t id = hist_end_id(&h) - 1; + assert(term_hist_decode_segment(&h, id, 0, 6, out, NULL) == 6); + assert(out[0].c == 'P' && out[5].c == 'X'); + assert(term_hist_line_rows(&h, id, 1000) == 11); + term_hist_free(&h); +} + +static void test_nul_and_wide_pop(void) +{ + struct hist h; + struct cell out[8]; + assert(term_hist_init(&h, 10, 64 * 1024)); + struct cell nul_row[4] = { dc('A'), dc(0), dc('B'), dc('C') }; + term_hist_push_row(&h, nul_row, 4, false, NULL); + assert(term_hist_decode_segment(&h, 0, 0, 4, out, NULL) == 4); + assert(out[0].c == 'A' && out[1].c == ' ' && out[2].c == 'B' && out[3].c == 'C'); + term_hist_free(&h); + + /* A pop does not split a wide cell. */ + assert(term_hist_init(&h, 10, 64 * 1024 * 4)); + struct cell wide[4] = { dc('A'), dc(0x4E2D), dc(0), dc('B') }; + wide[1].flags = CELL_WIDE; + wide[2].flags = CELL_TRAIL; + term_hist_push_row(&h, wide, 4, true, NULL); + struct cell popped[2]; + assert(term_hist_pop_tail_cells(&h, 2, popped, NULL) == 1); + assert(popped[0].c == 'B'); + assert(term_hist_decode_segment(&h, 0, 0, 4, out, NULL) == 3); + assert(out[0].c == 'A' && out[1].c == 0x4E2D && (out[2].flags & CELL_TRAIL)); + term_hist_free(&h); +} + +static void test_default_prefix_attributes(void) +{ + struct hist h; + assert(term_hist_init(&h, 100, 64 * 1024 * 4)); + struct cell row[8]; + for (int i = 0; i < 8; i++) { + row[i] = dc('a' + i); + if (i >= 4) { + row[i].fg = term_color_rgb(10, 20, 30); + row[i].bg = term_color_rgb(40, 50, 60); + row[i].flags = CELL_BOLD; + } + } + term_hist_push_row(&h, row, 8, false, NULL); + struct cell out[8]; + assert(term_hist_decode_segment(&h, 0, 0, 8, out, NULL) == 8); + for (int i = 0; i < 8; i++) { + assert(out[i].c == (uint32_t)('a' + i)); + assert(out[i].fg == row[i].fg && out[i].bg == row[i].bg && out[i].flags == row[i].flags); + } + + struct pen_attr colored = pen_attr_default(); + colored.fg = term_color_rgb(200, 100, 50); + term_hist_push_ascii(&h, "0123", 4, NULL, true); + term_hist_push_ascii(&h, "4567", 4, &colored, false); + assert(term_hist_decode_segment(&h, 1, 0, 8, out, NULL) == 8); + for (int i = 0; i < 8; i++) + assert(out[i].fg == (i < 4 ? TERM_COLOR_FG : term_color_rgb(200, 100, 50))); + term_hist_free(&h); +} + +static void test_utf8_and_disabled(void) +{ + struct hist h; + assert(term_hist_init(&h, 2, 64 * 1024 * 2)); + term_hist_push_ascii(&h, "first", 5, NULL, false); + struct cell p1[2] = { dc('A'), dc('B') }; + struct cell p2[2] = { dc(0x00e9), dc('C') }; + term_hist_push_row(&h, p1, 2, true, NULL); + term_hist_push_row(&h, p2, 2, false, NULL); + assert(lines(&h) == 2 && hist_first_id(&h) == 0); + struct cell out[8]; + assert(term_hist_decode_segment(&h, 1, 0, 8, out, NULL) == 4); + assert(out[0].c == 'A' && out[1].c == 'B' && out[2].c == 0x00e9 && out[3].c == 'C'); + term_hist_free(&h); + + assert(term_hist_init(&h, 0, 0)); + term_hist_push_ascii(&h, "ignored", 7, NULL, false); + term_hist_push_row(&h, p1, 2, false, NULL); + term_hist_push_blank(&h, 3); + assert(lines(&h) == 0); + term_hist_free(&h); +} + +/* Random rows of every kind, pushed and compared with a plain copy. */ +static void test_fuzz_rows(void) +{ + enum { LIMIT = 40, COLS = 24, ITER = 4000 }; + struct gstore *g = term_gstore_new(NULL); + uint32_t cps[2] = { 'a', 0x0301 }; + uint32_t cluster = term_gstore_intern(g, cps, 2); + struct hist h; + assert(term_hist_init(&h, LIMIT, 64 * 1024 * 2)); + + static struct cell ref[LIMIT + 1][COLS * 8]; + static int ref_len[LIMIT + 1]; + uint64_t ref_first = 0, ref_end = 0; + bool open = false; + unsigned state = 7; + for (int it = 0; it < ITER; it++) { + state = state * 1103515245u + 12345u; + int len = (int)((state >> 16) % (COLS + 1)); + bool wrapped = ((state >> 8) & 3) == 0; + struct cell row[COLS]; + int n = 0; + while (n < len) { + state = state * 1103515245u + 12345u; + unsigned k = (state >> 16) % 10; + struct cell c = dc('a' + (state >> 20) % 26); + if (k == 1) + c.fg = term_color_index((int)(state >> 24) % 16); + if (k == 2) + c.flags = CELL_BOLD; + if (k == 3) + c.c = 0xe9; + if (k == 4) + c.c = cluster; + if (k == 5 && n + 1 < len) { + c.c = 0x4e00 + (state >> 20) % 100; + c.flags = CELL_WIDE; + row[n++] = c; + c.c = 0; + c.flags = CELL_TRAIL; + } + row[n++] = c; + } + if (!open) { + if (ref_end - ref_first == LIMIT) + ref_first++; + ref_len[ref_end % (LIMIT + 1)] = 0; + ref_end++; + } + int slot = (int)((ref_end - 1) % (LIMIT + 1)); + if (!open && len == 0 && !wrapped) { + /* A blank line. */ + } else { + memcpy(&ref[slot][ref_len[slot]], row, (size_t)n * sizeof(row[0])); + ref_len[slot] += n; + } + term_hist_push_row(&h, row, n, wrapped, g); + open = wrapped; + assert(hist_first_id(&h) == ref_first && hist_end_id(&h) == ref_end); + } + + struct cell out[COLS * 8]; + for (uint64_t id = ref_first; id < ref_end; id++) { + int slot = (int)(id % (LIMIT + 1)); + int want = ref_len[slot]; + assert((int)term_hist_line_cells(&h, id) == want); + if (!want) + continue; + assert(term_hist_decode_segment(&h, id, 0, COLS * 8, out, g) == want); + for (int x = 0; x < want; x++) { + const struct cell *a = &ref[slot][x], *b = &out[x]; + if (a->flags & CELL_TRAIL) { + assert(b->flags & CELL_TRAIL); + continue; + } + assert(a->fg == b->fg && a->flags == b->flags); + if (grapheme_is_handle(a->c)) { + uint32_t got[GRAPHEME_MAX_LEN]; + assert(term_gstore_expand(g, b->c, got, GRAPHEME_MAX_LEN) == 2 && got[1] == 0x0301); + } else { + assert(a->c == b->c); + } + } + } + term_hist_free(&h); + term_gstore_free(g); +} + +static void test_large_wide_count(void) +{ + struct hist h; + struct hist_view view; + assert(term_hist_init(&h, 600, 4 * HIST_PAGE_SIZE)); + term_hist_view_init(&view); + struct cell row[4] = { dc('A'), dc(0x65e5), dc(0), dc('B') }; + row[1].flags = CELL_WIDE; + row[2].flags = CELL_TRAIL; + for (int i = 0; i < 400; i++) + term_hist_push_row(&h, row, 4, true, NULL); + term_hist_close_open_line(&h); + uint64_t exact = term_hist_rows(&h, 3); + term_hist_prepare_rows(&h, 3); + assert(term_hist_estimated_rows(&h, 3) < exact); + assert(term_hist_view_sync(&view, &h, 3)); + term_hist_view_scroll_to(&view, &h, 3, (struct line_pos){0}, INT_MAX); + term_hist_view_refine(&view, &h, 3, 3); + assert(exact > 254); + assert(term_hist_estimated_rows(&h, 3) == exact); + for (int i = 0; i < 300; i++) + term_hist_push_ascii(&h, "x", 1, NULL, false); + assert(term_hist_estimated_rows(&h, 3) == exact + 300); + assert(term_hist_view_sync(&view, &h, 3)); + for (int i = 0; i < 300; i++) + term_hist_push_ascii(&h, "x", 1, NULL, false); + assert(term_hist_estimated_rows(&h, 3) == 600); + term_hist_view_free(&view); + term_hist_free(&h); +} + +static void test_view_after_prefix_eviction(void) +{ + struct hist h; + struct hist_view view; + assert(term_hist_init(&h, 10, 2 * HIST_PAGE_SIZE)); + term_hist_view_init(&view); + char bytes[1000]; + for (int i = 0; i < 100; i++) { + memset(bytes, 'A' + i % 26, sizeof(bytes)); + term_hist_push_ascii(&h, bytes, sizeof(bytes), NULL, true); + } + term_hist_view_scroll_to(&view, &h, 1000, (struct line_pos){0}, 10); + uint64_t id; + int seg, screen_y; + struct cell before[1000], after[1000]; + assert(term_hist_view_row_pos(&view, &h, 1000, 3, (struct line_pos){0}, + 0, &id, &seg, &screen_y)); + assert(term_hist_decode_segment(&h, id, seg, 1000, before, NULL) == 1000); + for (int i = 100; i < 140; i++) { + memset(bytes, 'A' + i % 26, sizeof(bytes)); + term_hist_push_ascii(&h, bytes, sizeof(bytes), NULL, true); + term_hist_view_note_push(&view, &h, 1000); + } + assert(term_hist_line_cells(&h, 0) < 140000); + assert(term_hist_view_row_pos(&view, &h, 1000, 3, (struct line_pos){0}, + 0, &id, &seg, &screen_y)); + assert(term_hist_decode_segment(&h, id, seg, 1000, after, NULL) == 1000); + for (int i = 0; i < 1000; i++) + assert(before[i].c == after[i].c); + term_hist_view_rewrap(&view, &h, 500); + assert(term_hist_view_row_pos(&view, &h, 500, 3, (struct line_pos){0}, + 0, &id, &seg, &screen_y)); + assert(term_hist_decode_segment(&h, id, seg, 500, after, NULL) == 500); + for (int i = 0; i < 500; i++) + assert(before[i].c == after[i].c); + term_hist_view_free(&view); + term_hist_free(&h); +} + +static void test_short_lines_after_eviction(void) +{ + struct hist h; + assert(term_hist_init(&h, 64, 64 * 1024 * 4)); + term_hist_prepare_rows(&h, 80); + const char text[] = "abc\n\nx\npqrs\n\nz\n"; + const int offsets[] = {0, 4, 5, 7, 12, 13}; + const int lengths[] = {3, 0, 1, 4, 0, 1}; + struct hist_ascii_line batch[6]; + for (int i = 0; i < 6; i++) + batch[i] = (struct hist_ascii_line){ + .ptr = text + offsets[i], .len = (uint16_t)lengths[i], + }; + for (int pass = 0; pass < 20000; pass++) + term_hist_push_ascii_lines(&h, batch, 6, NULL); + assert(lines(&h) == 64); + assert(term_hist_rows(&h, 80) == 64); + for (uint64_t id = hist_first_id(&h); id < hist_end_id(&h); id++) { + int k = (int)(id % 6); + struct cell out[80]; + assert(term_hist_decode_segment(&h, id, 0, 80, out, NULL) == lengths[k]); + for (int x = 0; x < lengths[k]; x++) { + assert(out[x].c == (uint32_t)text[offsets[k] + x]); + assert(out[x].fg == TERM_COLOR_FG); + assert(out[x].bg == TERM_COLOR_BG); + } + } + term_hist_free(&h); +} + +int main(void) +{ + test_short_lines_after_eviction(); + test_large_wide_count(); + test_view_after_prefix_eviction(); + test_init_free(); + test_ascii_roundtrip(); + test_attributes_roundtrip(); + test_wide_and_wrap(); + test_clusters_roundtrip(); + test_open_lines_and_pop(); + test_open_line_joins(); + test_eviction(); + test_width_one(); + test_emoji_and_text_presentation(); + test_blank_lines(); + test_open_line_attrs_continuation(); + test_hyperlinks(); + test_long_lines(); + test_page_limits(); + test_nul_and_wide_pop(); + test_default_prefix_attributes(); + test_utf8_and_disabled(); + test_fuzz_rows(); + + printf("PASS: hist tests\n"); + return 0; +} -- cgit v1.2.3