diff options
| author | Berke Güzel <wenekar1@gmail.com> | 2026-10-15 12:39:21 +0300 |
|---|---|---|
| committer | Berke Güzel <wenekar1@gmail.com> | 2026-10-15 12:39:21 +0300 |
| commit | ca69844a243a6e335b0ddafad057f35e89e0a576 (patch) | |
| tree | 7192c9f05e6500bb2ba550ca976ed9951b87c1f4 /tests/snapshot_failure_test.c | |
| parent | 22345caf457e8d9a4bdb56b696cc5f3694812276 (diff) | |
| parent | 4c72f7b02fbafd15d0cf843edece106714bcc5f7 (diff) | |
Merge branch 'wt/snapshot-atomic' into api-hardening
# Conflicts:
# src/snapshot.c
Diffstat (limited to 'tests/snapshot_failure_test.c')
| -rw-r--r-- | tests/snapshot_failure_test.c | 467 |
1 files changed, 400 insertions, 67 deletions
diff --git a/tests/snapshot_failure_test.c b/tests/snapshot_failure_test.c index a516ec2..6ce8f7b 100644 --- a/tests/snapshot_failure_test.c +++ b/tests/snapshot_failure_test.c @@ -3,91 +3,424 @@ #include <assert.h> #include <stdbool.h> #include <stddef.h> +#include <stdint.h> #include <stdlib.h> +#include <string.h> /* --wrap redirects calls after LTO has analyzed their side effects. Keep - * these accesses observable so it cannot fold away the wrapper's changes. */ -static volatile bool fail_next_realloc; -static volatile int malloc_failure_countdown = -1; -static volatile bool malloc_was_failed; + * these accesses observable so it cannot fold away the wrapper's changes. + * The countdown fails the allocation after it reaches zero. malloc and + * realloc share it, so a sweep over the countdown hits every site. */ +static volatile int alloc_countdown = -1; +static volatile bool alloc_was_failed; + +static bool alloc_should_fail(void) +{ + if (alloc_countdown < 0) + return false; + if (alloc_countdown > 0) { + alloc_countdown--; + return false; + } + alloc_countdown = -1; + alloc_was_failed = true; + return true; +} + void *__real_malloc(size_t); void *__wrap_malloc(size_t size) { - if (malloc_failure_countdown >= 0) { - if (malloc_failure_countdown-- == 0) { - malloc_failure_countdown = -1; - malloc_was_failed = true; - return NULL; - } - } + if (alloc_should_fail()) + return NULL; return __real_malloc(size); } void *__real_realloc(void *, size_t); void *__wrap_realloc(void *p, size_t size) { - if (fail_next_realloc) { - fail_next_realloc = false; + if (alloc_should_fail()) return NULL; - } return __real_realloc(p, size); } -int main(void) +/* Code points of a cluster cell are copied up to this many. */ +#define CPS_MAX 32 +#define CPS_SLOT (CPS_MAX + 1) + +/* A copy of everything a renderer can read from a snapshot. */ +struct frame { + struct term_view view; + uint32_t palette[TERM_PALETTE_SIZE]; + int cols, rows; + struct term_cell *cells; /* rows * cols */ + uint32_t *cps; /* per cell: count, then code points */ + uint64_t *keys; /* per row */ + struct term_span *sel; /* per row */ +}; + +static void frame_capture(const struct term_snapshot *s, struct frame *f) +{ + const struct term_view *v = term_snapshot_view(s); + f->view = *v; + memcpy(f->palette, v->palette, sizeof(f->palette)); + f->cols = v->size.x; + f->rows = v->size.y; + size_t n = (size_t)f->cols * (size_t)f->rows; + f->cells = calloc(n + 1, sizeof(*f->cells)); + f->cps = calloc(n + 1, CPS_SLOT * sizeof(*f->cps)); + f->keys = calloc((size_t)f->rows + 1, sizeof(*f->keys)); + f->sel = calloc((size_t)f->rows + 1, sizeof(*f->sel)); + assert(f->cells && f->cps && f->keys && f->sel); + for (int y = 0; y < f->rows; y++) { + const struct term_cell *row = term_snapshot_row(s, y); + assert(row); + f->keys[y] = term_snapshot_row_key(s, y); + f->sel[y] = term_snapshot_row_selection(s, y); + for (int x = 0; x < f->cols; x++) { + size_t i = (size_t)y * f->cols + x; + f->cells[i] = row[x]; + uint32_t *cps = f->cps + i * CPS_SLOT; + cps[0] = 0; + if (row[x].text & TERM_TEXT_CLUSTER) + cps[0] = (uint32_t)term_snapshot_cluster(s, row[x].text, cps + 1, CPS_MAX); + } + } +} + +static void frame_free(struct frame *f) +{ + free(f->cells); + free(f->cps); + free(f->keys); + free(f->sel); +} + +static bool same_view(const struct frame *a, const struct frame *b) +{ + const struct term_view *x = &a->view, *y = &b->view; + return x->size.x == y->size.x && x->size.y == y->size.y && + x->cursor.pos.x == y->cursor.pos.x && x->cursor.pos.y == y->cursor.pos.y && + x->cursor.style == y->cursor.style && x->cursor.visible == y->cursor.visible && + x->cursor.blink == y->cursor.blink && + x->scroll_offset == y->scroll_offset && x->scrolled == y->scrolled && + x->reverse_video == y->reverse_video && x->version == y->version && + x->palette_version == y->palette_version && + memcmp(a->palette, b->palette, sizeof(a->palette)) == 0; +} + +/* The same state as before a call: view, cells, keys, selection, and the + * arena records behind the cluster cells. */ +static bool frame_same(const struct frame *a, const struct frame *b) +{ + if (!same_view(a, b)) + return false; + size_t n = (size_t)a->cols * (size_t)a->rows; + return memcmp(a->cells, b->cells, n * sizeof(*a->cells)) == 0 && + memcmp(a->cps, b->cps, n * CPS_SLOT * sizeof(*a->cps)) == 0 && + memcmp(a->keys, b->keys, (size_t)a->rows * sizeof(*a->keys)) == 0 && + memcmp(a->sel, b->sel, (size_t)a->rows * sizeof(*a->sel)) == 0; +} + +/* What a renderer paints. Arena offsets, keys, and version numbers may differ + * between an incremental snapshot and a fresh one. */ +static bool frame_shows_same(const struct frame *a, const struct frame *b) +{ + const struct term_view *x = &a->view, *y = &b->view; + if (a->cols != b->cols || a->rows != b->rows || x->cursor.pos.x != y->cursor.pos.x || + x->cursor.pos.y != y->cursor.pos.y || x->cursor.visible != y->cursor.visible || + x->scroll_offset != y->scroll_offset || x->reverse_video != y->reverse_video || + memcmp(a->palette, b->palette, sizeof(a->palette)) != 0) + return false; + for (int r = 0; r < a->rows; r++) { + if (memcmp(&a->sel[r], &b->sel[r], sizeof(a->sel[r])) != 0) + return false; + for (int c = 0; c < a->cols; c++) { + size_t i = (size_t)r * a->cols + c; + const struct term_cell *p = &a->cells[i], *q = &b->cells[i]; + const uint32_t *cp = a->cps + i * CPS_SLOT, *cq = b->cps + i * CPS_SLOT; + if (p->fg != q->fg || p->bg != q->bg || p->attrs != q->attrs || p->width != q->width) + return false; + if (p->text & TERM_TEXT_CLUSTER) { + if (cp[0] != cq[0] || memcmp(cp + 1, cq + 1, cp[0] * sizeof(*cp)) != 0) + return false; + } else if (p->text != q->text) { + return false; + } + } + } + return true; +} + +/* One update with the k-th allocation failing, from the state the caller + * built. A failed update must leave s as it was, and the retry must succeed. + * Returns true when an allocation was failed. */ +static bool update_failing_at(struct term_snapshot *s, struct term *t, int k) +{ + struct frame before, after; + frame_capture(s, &before); + alloc_was_failed = false; + alloc_countdown = k; + bool ok = term_snapshot_update(s, t); + alloc_countdown = -1; + bool injected = alloc_was_failed; + if (!ok) { + assert(injected); + frame_capture(s, &after); + assert(frame_same(&before, &after)); + frame_free(&after); + assert(term_snapshot_update(s, t)); + } + frame_free(&before); + return injected; +} + +/* Run attempt(k) for k = 0, 1, ... until one injects no failure. Each + * attempt builds its own state, so every site fails from the same start. + * Returns the number of failed updates. */ +static int sweep(bool (*attempt)(int k)) +{ + int k = 0; + while (attempt(k)) { + k++; + assert(k < 1000); + } + return k; +} + +/* Update s from t with the k-th allocation failing, for k = 0, 1, ... until + * an update succeeds. Every failed attempt must leave s as it was. Progress + * from earlier attempts stays, so this covers the later steps of a long run + * rather than each site from one start. Returns the number of failures. */ +static int update_each_failure(struct term_snapshot *s, struct term *t) +{ + struct frame before, after; + frame_capture(s, &before); + int failures = 0; + bool ok = false; + for (int k = 0; !ok; k++) { + assert(k < 1000); + alloc_was_failed = false; + alloc_countdown = k; + ok = term_snapshot_update(s, t); + alloc_countdown = -1; + if (ok) + break; + assert(alloc_was_failed); + failures++; + frame_capture(s, &after); + assert(frame_same(&before, &after)); + frame_free(&after); + } + frame_free(&before); + return failures; +} + +/* A renderer sees the same picture as a fresh capture of t. */ +static void expect_matches_fresh(struct term *t, const struct term_snapshot *s) +{ + struct term_snapshot *fresh = term_snapshot_new(); + assert(fresh && term_snapshot_update(fresh, t)); + struct frame a, b; + frame_capture(s, &a); + frame_capture(fresh, &b); + assert(frame_shows_same(&a, &b)); + frame_free(&a); + frame_free(&b); + term_snapshot_free(fresh); +} + +/* A US flag and a ZWJ family: two regional indicators, and five code points. */ +static const char flag_us[] = "\xf0\x9f\x87\xba\xf0\x9f\x87\xb8"; +static const char family[] = "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7"; + +static uint32_t cluster_cps(const struct term_snapshot *s, const struct term_cell *cell, + uint32_t *cps, int cap) +{ + return (uint32_t)term_snapshot_cluster(s, cell->text, cps, cap); +} + +/* A new terminal and an empty snapshot. The first update grows every buffer + * and the arena, so each allocation site in the update is hit. */ +static bool first_update_at(int k) { struct term_options options = { .size = { 8, 3 }, .scrollback = 5 }; - struct term *term = term_new(&options); - struct term_snapshot *snapshot = term_snapshot_new(); - assert(term && snapshot && term_snapshot_update(snapshot, term)); - - const char accented[] = "e\xcc\x81"; - term_feed(term, accented, sizeof(accented) - 1, 0); - fail_next_realloc = true; - assert(!term_snapshot_update(snapshot, term)); - assert(!fail_next_realloc); - assert(term_snapshot_view(snapshot)->size.x == 0); - assert(term_snapshot_view(snapshot)->size.y == 0); - assert(term_snapshot_row(snapshot, 0) == NULL); - - assert(term_snapshot_update(snapshot, term)); - const struct term_cell *row = term_snapshot_row(snapshot, 0); - assert(row); + struct term *t = term_new(&options); + struct term_snapshot *s = term_snapshot_new(); + assert(t && s); + + const char accented[] = "e\xcc\x81 "; + term_feed(t, accented, sizeof(accented) - 1, 0); + term_feed(t, flag_us, sizeof(flag_us) - 1, 0); + + bool injected = update_failing_at(s, t, k); + + const struct term_cell *row = term_snapshot_row(s, 0); + assert(row && (row[0].text & TERM_TEXT_CLUSTER)); + uint32_t cps[8]; + assert(cluster_cps(s, &row[0], cps, 8) == 2 && cps[0] == 'e' && cps[1] == 0x301); + assert(cluster_cps(s, &row[2], cps, 8) == 2 && cps[0] == 0x1F1FA && cps[1] == 0x1F1F8); + expect_matches_fresh(t, s); + + term_snapshot_free(s); + term_free(t); + return injected; +} + +/* A 32 by 16 screen full of clusters. The arena needs more than its first + * 256 words, so the arena growth is one of the failing sites. */ +static bool full_cluster_screen_at(int k) +{ + struct term_options options = { .size = { 32, 16 }, .scrollback = 5 }; + struct term *t = term_new(&options); + struct term_snapshot *s = term_snapshot_new(); + assert(t && s); + + for (int y = 0; y < 16; y++) { + for (int i = 0; i < 16; i++) { + if (y % 2) + term_feed(t, flag_us, sizeof(flag_us) - 1, 0); + else + term_feed(t, family, sizeof(family) - 1, 0); + } + if (y < 15) + term_feed(t, "\r\n", 2, 0); + } + + bool injected = update_failing_at(s, t, k); + + uint32_t cps[8]; + const struct term_cell *row = term_snapshot_row(s, 0); + assert(cluster_cps(s, &row[0], cps, 8) == 5 && cps[0] == 0x1F468 && cps[1] == 0x200D); + row = term_snapshot_row(s, 1); + assert(cluster_cps(s, &row[0], cps, 8) == 2 && cps[0] == 0x1F1FA); + expect_matches_fresh(t, s); + + term_snapshot_free(s); + term_free(t); + return injected; +} + +/* The screen grows, so cells, keys, row clusters, and selection all reserve. */ +static bool resize_at(int k) +{ + struct term_options options = { .size = { 8, 3 }, .scrollback = 5 }; + struct term *t = term_new(&options); + struct term_snapshot *s = term_snapshot_new(); + assert(t && s); + term_feed(t, "old", 3, 0); + assert(term_snapshot_update(s, t)); + + assert(term_resize(t, (struct term_vector2i){ 32, 16 })); + term_feed(t, "\x1b[H\x1b[2J", 7, 0); + term_feed(t, family, sizeof(family) - 1, 0); + + bool injected = update_failing_at(s, t, k); + + assert(term_snapshot_view(s)->size.x == 32 && term_snapshot_view(s)->size.y == 16); + uint32_t cps[8]; + const struct term_cell *row = term_snapshot_row(s, 0); + assert(cluster_cps(s, &row[0], cps, 8) == 5 && cps[0] == 0x1F468); + expect_matches_fresh(t, s); + + term_snapshot_free(s); + term_free(t); + return injected; +} + +/* More than 256 rows moves the scratch keys and spans off the stack. The + * selection is left out: its own row list falls back to 256 rows when its + * allocation fails, which is outside the snapshot. */ +static bool tall_screen_at(int k) +{ + struct term_options options = { .size = { 8, 300 }, .scrollback = 5 }; + struct term *t = term_new(&options); + struct term_snapshot *s = term_snapshot_new(); + assert(t && s); + for (int y = 0; y < 299; y++) + term_feed(t, "\r\n", 2, 0); + term_feed(t, "e\xcc\x81yes", 5, 0); + + bool injected = update_failing_at(s, t, k); + + assert(term_snapshot_view(s)->size.y == 300); + const struct term_cell *row = term_snapshot_row(s, 299); uint32_t cps[4]; - assert(term_snapshot_cluster(snapshot, row[0].text, cps, 4) == 2); - assert(cps[0] == 'e' && cps[1] == 0x301); - - term_snapshot_free(snapshot); - term_free(term); - - /* More than 256 rows moves both temporary arrays off the stack. */ - options.size.y = 300; - term = term_new(&options); - snapshot = term_snapshot_new(); - assert(term && snapshot && term_snapshot_update(snapshot, term)); - - /* Fail new_keys allocation, then make sure retry fills all rows. */ - malloc_was_failed = false; - malloc_failure_countdown = 0; - assert(!term_snapshot_update(snapshot, term)); - assert(malloc_was_failed); - assert(term_snapshot_view(snapshot)->size.y == 0); - assert(term_snapshot_row(snapshot, 0) == NULL); - assert(term_snapshot_update(snapshot, term)); - assert(term_snapshot_view(snapshot)->size.y == 300); - assert(term_snapshot_row(snapshot, 299)); - - /* The first malloc is new_keys; fail the following selection-span array. */ - malloc_was_failed = false; - malloc_failure_countdown = 1; - assert(!term_snapshot_update(snapshot, term)); - assert(malloc_was_failed); - assert(term_snapshot_view(snapshot)->size.y == 0); - assert(term_snapshot_row(snapshot, 0) == NULL); - assert(term_snapshot_update(snapshot, term)); - assert(term_snapshot_view(snapshot)->size.y == 300); - assert(term_snapshot_row(snapshot, 299)); - - term_snapshot_free(snapshot); - term_free(term); + assert(cluster_cps(s, &row[0], cps, 4) == 2 && cps[0] == 'e' && cps[1] == 0x301); + expect_matches_fresh(t, s); + + term_snapshot_free(s); + term_free(t); + return injected; +} + +/* Without failures, the selection of a tall screen reaches the last row. */ +static void tall_selection_is_kept(void) +{ + struct term_options options = { .size = { 8, 300 }, .scrollback = 5 }; + struct term *t = term_new(&options); + struct term_snapshot *s = term_snapshot_new(); + assert(t && s); + for (int y = 0; y < 299; y++) + term_feed(t, "\r\n", 2, 0); + term_feed(t, "e\xcc\x81yes", 5, 0); + term_select_start(t, (struct term_vector2i){ .x = 0, .y = 299 }, false, TERM_SELECT_CELL); + term_select_extend(t, (struct term_vector2i){ .x = 3, .y = 299 }, true); + + assert(term_snapshot_update(s, t)); + struct term_span span = term_snapshot_row_selection(s, 299); + assert(span.begin == 0 && span.end == 4); + expect_matches_fresh(t, s); + + term_snapshot_free(s); + term_free(t); +} + +/* Scroll with clusters in most lines. Each step fails every allocation in + * turn, then retries, and must match a fresh capture. */ +static void scrolling_fails_cleanly(void) +{ + struct term_options options = { .size = { 8, 3 }, .scrollback = 5 }; + struct term *t = term_new(&options); + struct term_snapshot *s = term_snapshot_new(); + assert(t && s); + + int failures = 0; + for (int i = 0; i < 300; i++) { + term_feed(t, "e\xcc\x81", 3, 0); + if (i % 3 == 0) + term_feed(t, flag_us, sizeof(flag_us) - 1, 0); + if (i % 5 == 0) + term_feed(t, family, sizeof(family) - 1, 0); + term_feed(t, "\r\n", 2, 0); + if (i == 150) + term_select_start(t, (struct term_vector2i){ .x = 0, .y = 1 }, false, TERM_SELECT_CELL); + if (i == 151) + term_select_extend(t, (struct term_vector2i){ .x = 4, .y = 2 }, true); + failures += update_each_failure(s, t); + expect_matches_fresh(t, s); + } + assert(failures > 0); + assert(term_history_rows(t) == 5); + + term_snapshot_free(s); + term_free(t); +} + +int main(void) +{ + /* Four reserve() buffers and the arena. */ + int failures = sweep(first_update_at); + assert(failures >= 5); + /* Four reserve() buffers and the arena, with a larger arena. */ + failures = sweep(full_cluster_screen_at); + assert(failures >= 5); + /* Four reserve() buffers and the arena, after the screen grows. */ + failures = sweep(resize_at); + assert(failures >= 5); + /* Four reserve() buffers, the scratch array, and the arena. */ + failures = sweep(tall_screen_at); + assert(failures >= 6); + tall_selection_is_kept(); + scrolling_fails_cleanly(); return 0; } |
