summaryrefslogtreecommitdiff
path: root/tests/write_fuzz_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/write_fuzz_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/write_fuzz_test.c')
-rw-r--r--tests/write_fuzz_test.c119
1 files changed, 119 insertions, 0 deletions
diff --git a/tests/write_fuzz_test.c b/tests/write_fuzz_test.c
new file mode 100644
index 0000000..8c1475a
--- /dev/null
+++ b/tests/write_fuzz_test.c
@@ -0,0 +1,119 @@
+/*
+ * Differential fuzz test for term_feed. Each seed makes random terminal
+ * output: text, controls, CSI and ESC sequences, raw bytes, and resizes.
+ * One terminal gets the output in a few large writes; the bulk fast paths
+ * handle it. A second terminal gets it one byte at a time; the generic
+ * parser handles it. The two must show the same cursor, screen, and
+ * scrollback.
+ */
+#include "term_api.h"
+#include "fuzz_gen.h"
+
+#include <assert.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Each terminal has its own snapshot, so cells of both stay readable at
+ * the same time. */
+static struct term_snapshot *snap_bulk, *snap_bytes;
+
+/* The code points of a cell's text. A cluster has handles that differ
+ * between snapshots, so the test compares its code points. */
+static int cell_text(const struct term_snapshot *s, const struct term_cell *cell,
+ uint32_t *cps, int cap)
+{
+ int n = term_snapshot_cluster(s, cell->text, cps, cap);
+ return n < cap ? n : cap;
+}
+
+static bool same_cell(const struct term_cell *a, const struct term_cell *b)
+{
+ if (a->fg != b->fg || a->bg != b->bg || a->attrs != b->attrs || a->width != b->width)
+ return false;
+ uint32_t ta[16], tb[16];
+ int na = cell_text(snap_bulk, a, ta, 16);
+ int nb = cell_text(snap_bytes, b, tb, 16);
+ return na == nb && memcmp(ta, tb, (size_t)na * sizeof(ta[0])) == 0;
+}
+
+static void assert_same(struct term *bulk, struct term *bytes, int seed, int step)
+{
+ int cols = tt_cols(bulk), rows = tt_rows(bulk);
+ int history = tt_history(bulk);
+ bool same = tt_cursor_x(bulk) == tt_cursor_x(bytes) &&
+ tt_cursor_y(bulk) == tt_cursor_y(bytes) &&
+ history == tt_history(bytes);
+ term_scroll(bulk, history);
+ term_scroll(bytes, history);
+ for (int page = 0; same && page <= history; page += rows) {
+ bool copied = term_snapshot_update(snap_bulk, bulk);
+ copied &= term_snapshot_update(snap_bytes, bytes);
+ assert(copied);
+ for (int y = 0; same && y < rows; y++) {
+ const struct term_cell *ra = term_snapshot_row(snap_bulk, y);
+ const struct term_cell *rb = term_snapshot_row(snap_bytes, y);
+ for (int x = 0; x < cols; x++) {
+ const struct term_cell *a = &ra[x], *b = &rb[x];
+ if (!same_cell(a, b)) {
+ fprintf(stderr, "seed %d step %d: page %d row %d col %d: "
+ "bulk %x/%x/%x/%x/%u, bytes %x/%x/%x/%x/%u\n",
+ seed, step, page, y, x, a->text, a->fg, a->bg, a->attrs,
+ a->width, b->text, b->fg, b->bg, b->attrs, b->width);
+ same = false;
+ break;
+ }
+ }
+ }
+ term_scroll(bulk, -rows);
+ term_scroll(bytes, -rows);
+ }
+ term_scroll_to(bulk, 0);
+ term_scroll_to(bytes, 0);
+ if (!same)
+ fprintf(stderr, "seed %d step %d: cursor %d,%d vs %d,%d, history %d vs %d\n",
+ seed, step, tt_cursor_x(bulk), tt_cursor_y(bulk),
+ tt_cursor_x(bytes), tt_cursor_y(bytes),
+ history, tt_history(bytes));
+ assert(same);
+}
+
+int main(int argc, char **argv)
+{
+ int seeds = argc > 1 ? atoi(argv[1]) : 50000;
+ for (int seed = 1; seed <= seeds; seed++) {
+ rng = 0x9e3779b97f4a7c15ull * (uint64_t)seed;
+ int cols = 4 + rn(30), rows = 2 + rn(10);
+ struct term *bulk = tt_new(cols, rows);
+ struct term *bytes = tt_new(cols, rows);
+ /* A new terminal can get the address of a freed one, so each seed
+ * uses new snapshots. */
+ snap_bulk = term_snapshot_new();
+ snap_bytes = term_snapshot_new();
+ assert(bulk && bytes && snap_bulk && snap_bytes);
+ for (int step = 0; step < 6; step++) {
+ blen = 0;
+ gen(20 + rn(60), cols, rows);
+ size_t split = blen ? (size_t)rn((int)blen + 1) : 0;
+ tt_write(bulk, buf, split);
+ tt_write(bulk, buf + split, blen - split);
+ for (size_t i = 0; i < blen; i++)
+ tt_write(bytes, buf + i, 1);
+ assert_same(bulk, bytes, seed, step);
+ if (rn(4) == 0) {
+ cols = 4 + rn(30);
+ rows = 2 + rn(10);
+ bool resized = tt_resize(bulk, cols, rows);
+ resized &= tt_resize(bytes, cols, rows);
+ assert(resized);
+ assert_same(bulk, bytes, seed, step);
+ }
+ }
+ term_snapshot_free(snap_bulk);
+ term_snapshot_free(snap_bytes);
+ term_free(bulk);
+ term_free(bytes);
+ }
+ return 0;
+}