diff options
| -rw-r--r-- | docs/correctness-audit.md | 5 | ||||
| -rw-r--r-- | include/term/term.h | 2 | ||||
| -rw-r--r-- | src/snapshot.c | 283 | ||||
| -rw-r--r-- | tests/correctness_probe.c | 9 | ||||
| -rw-r--r-- | tests/snapshot_failure_test.c | 467 |
5 files changed, 563 insertions, 203 deletions
diff --git a/docs/correctness-audit.md b/docs/correctness-audit.md index 4c9a14c..dd854a1 100644 --- a/docs/correctness-audit.md +++ b/docs/correctness-audit.md @@ -96,8 +96,9 @@ remain on row 3, and history should remain empty. - Snapshot scroll accounting now counts discarded cluster records as dead, allowing compaction to reclaim them. -- Snapshot allocation failures now report false, empty the snapshot, and - force a complete capture on retry. +- Snapshot allocation failures now report false and leave the previous + snapshot unchanged. Every allocation happens before a field changes, so a + retry is an ordinary incremental update. - Height shrink allocates the replacement screen before moving rows or changing the cursor, preserving state if allocation fails. diff --git a/include/term/term.h b/include/term/term.h index dc41114..2447b72 100644 --- a/include/term/term.h +++ b/include/term/term.h @@ -382,7 +382,7 @@ TERM_API void term_snapshot_free(struct term_snapshot *s); /* Copy the viewport of t into s. Keep rows with unchanged content keys. * Update the history view index and refine counts for visible wide lines. - * Return false when memory runs out. The snapshot is then empty. */ + * Return false when memory runs out. The snapshot then keeps its previous content. */ TERM_API bool term_snapshot_update(struct term_snapshot *s, struct term *t); /* Palette entries of struct term_view.palette after the 256 indexed colors. */ diff --git a/src/snapshot.c b/src/snapshot.c index 8dbb498..82a2805 100644 --- a/src/snapshot.c +++ b/src/snapshot.c @@ -46,6 +46,8 @@ void term_snapshot_free(struct term_snapshot *s) free(s); } +/* Make room for cols x rows. A failed call keeps the old contents, so the + * snapshot stays readable. */ static bool reserve(struct term_snapshot *s, int cols, int rows) { size_t cells = (size_t)cols * (size_t)rows; @@ -73,38 +75,13 @@ static bool reserve(struct term_snapshot *s, int cols, int rows) return true; } -/* The public contract makes every failed update an empty, retryable snapshot. */ -static void invalidate(struct term_snapshot *s) -{ - s->view = (struct term_view){ .palette = s->palette }; - s->source = 0; - s->arena_len = 0; - s->arena_dead = 0; - if (s->row_cap > 0) { - memset(s->keys, 0, (size_t)s->row_cap * sizeof(*s->keys)); - memset(s->row_clusters, 0, (size_t)s->row_cap * sizeof(*s->row_clusters)); - memset(s->selection, 0, (size_t)s->row_cap * sizeof(*s->selection)); - } -} - -static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint32_t handle, - bool *ok) +/* Append a cluster record. The caller reserved room for it. */ +static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint32_t handle) { uint32_t cps[GRAPHEME_MAX_LEN]; int n = term_gstore_expand(g, handle, cps, GRAPHEME_MAX_LEN); if (n <= 0) return ' '; - if (s->arena_len + (uint32_t)n + 1 > s->arena_cap) { - uint32_t cap = term_grow_cap32(s->arena_cap ? s->arena_cap * 2 : 256, - s->arena_len + (uint32_t)n + 1); - uint32_t *a = realloc(s->arena, cap * sizeof(*a)); - if (!a) { - *ok = false; - return 0; - } - s->arena = a; - s->arena_cap = cap; - } uint32_t off = s->arena_len; s->arena[off] = (uint32_t)n; memcpy(&s->arena[off + 1], cps, (size_t)n * sizeof(*cps)); @@ -112,10 +89,26 @@ static uint32_t arena_add(struct term_snapshot *s, const struct gstore *g, uint3 return TERM_TEXT_CLUSTER | off; } +/* Arena words that converting src adds. Counts what arena_add() writes. */ +static uint64_t row_words(const struct gstore *g, const struct cell *src, int cols) +{ + uint32_t cps[GRAPHEME_MAX_LEN]; + uint64_t words = 0; + for (int x = 0; x < cols; x++) { + uint32_t text = src[x].c; + if (!grapheme_is_handle(text)) + continue; + int n = term_gstore_expand(g, text, cps, GRAPHEME_MAX_LEN); + if (n > 0) + words += (uint64_t)n + 1; + } + return words; +} + /* Convert one stored row; returns the number of arena words it added. */ static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g, struct term_cell *dst, - const struct cell *src, int cols, bool *ok) + const struct cell *src, int cols) { uint32_t before = s->arena_len; for (int x = 0; x < cols; x++) { @@ -124,11 +117,8 @@ static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g, uint32_t text = c->c; if (TERM_EXPECT(text < 0x20, 0)) text = text ? ' ' : 0; - else if (TERM_EXPECT(grapheme_is_handle(text), 0)) { - text = arena_add(s, g, text, ok); - if (!*ok) - return 0; - } + else if (TERM_EXPECT(grapheme_is_handle(text), 0)) + text = arena_add(s, g, text); dst[x] = (struct term_cell){ .text = text, .fg = c->fg, @@ -140,43 +130,129 @@ static uint32_t convert_row(struct term_snapshot *s, const struct gstore *g, return s->arena_len - before; } +/* What one update does. Phase 1 fills it in and changes no snapshot field. */ +struct plan { + bool all; /* rebuild every row and reset the arena */ + int scrolled; /* rows the content moved up */ + const uint64_t *keys; /* key of each new row */ + const int *c0, *c1; /* selected columns of each row */ +}; + +/* Phase 2: write the plan into the snapshot. Nothing here can fail. */ +static void apply(struct term_snapshot *s, const struct term *t, const struct plan *p) +{ + int cols = t->cols, rows = t->rows; + int k = p->scrolled; + if (p->all) { + s->arena_len = 0; + s->arena_dead = 0; + } + + bool changed = p->all; + if (k > 0) { + for (int y = 0; y < k; y++) + s->arena_dead += s->row_clusters[y]; + memmove(s->cells, s->cells + (size_t)k * cols, + (size_t)(rows - k) * cols * sizeof(struct term_cell)); + memmove(s->row_clusters, s->row_clusters + k, + (size_t)(rows - k) * sizeof(uint32_t)); + memmove(s->keys, s->keys + k, + (size_t)(rows - k) * sizeof(uint64_t)); + memset(s->keys + (rows - k), 0, (size_t)k * sizeof(uint64_t)); + memset(s->row_clusters + (rows - k), 0, (size_t)k * sizeof(uint32_t)); + } + + for (int y = 0; y < rows; y++) { + uint64_t key = p->keys[y]; + if (!p->all && s->keys[y] == key) + continue; + if (!p->all) + s->arena_dead += s->row_clusters[y]; + const struct cell *src = term_screen_view_row(t, y); + s->row_clusters[y] = convert_row(s, t->gstore, s->cells + (size_t)y * cols, + src, cols); + s->keys[y] = key; + changed = true; + } + + for (int y = 0; y < rows; y++) { + struct term_span span = p->c1[y] >= p->c0[y] ? (struct term_span){ p->c0[y], p->c1[y] + 1 } + : (struct term_span){ 0 }; + if (p->all || span.begin != s->selection[y].begin || span.end != s->selection[y].end) + changed = true; + s->selection[y] = span; + } + + struct term_cursor cursor = term_cursor(t); + int view_offset = term_view_offset(t); + cursor.pos.y += view_offset; + if (cursor.pos.y >= rows) + cursor.visible = false; + const struct term_cursor *old = &s->view.cursor; + if (cursor.pos.x != old->pos.x || cursor.pos.y != old->pos.y || + cursor.style != old->style || cursor.visible != old->visible || + cursor.blink != old->blink || + s->view.scroll_offset != view_offset || + s->view.reverse_video != t->reverse_video) + changed = true; + if (p->all || s->view.palette_version != t->palette.version) { + memcpy(s->palette, t->palette.colors, sizeof(s->palette)); + s->view.palette_version = t->palette.version; + changed = true; + } + + s->source = t->serial; + s->view.size = (struct term_vector2i){ .x = cols, .y = rows }; + s->view.cursor = cursor; + s->view.scroll_offset = view_offset; + s->view.scrolled = k; + s->view.reverse_video = t->reverse_video; + if (changed || k) + s->view.version++; +} + +/* Phase 1 for one update. Allocates and checks everything that can fail, + * then hands the plan to apply(). A false return leaves the content as it was. */ static bool term_snapshot_update_inner(struct term_snapshot *s, struct term *t) { if (!s || !t) return false; int cols = t->cols, rows = t->rows; - if (!reserve(s, cols, rows)) { - invalidate(s); + bool ok = false; + bool resized = s->view.size.x != cols || s->view.size.y != rows; + bool all = resized || s->source != t->serial; + int scrolled = 0; + uint64_t need = 0; + + if (!reserve(s, cols, rows)) return false; + /* Scratch for rows above 256: new keys, then the selection spans. */ + uint64_t stack_keys[256]; + int stack[2][256]; + uint64_t *new_keys = stack_keys; + int *c0 = stack[0], *c1 = stack[1]; + void *heap = NULL; + if (rows > 256) { + heap = malloc((size_t)rows * (sizeof(*new_keys) + 2 * sizeof(*c0))); + if (!heap) + return false; + new_keys = heap; + c0 = (int *)(new_keys + rows); + c1 = c0 + rows; } + struct plan p = { .keys = new_keys, .c0 = c0, .c1 = c1 }; - bool resized = s->view.size.x != cols || s->view.size.y != rows; - bool all = resized || s->source != t->serial; /* Reclaim cluster records once most of the arena is garbage. */ if (s->arena_dead > 4096 && s->arena_dead * 2 > s->arena_len) all = true; - if (all) { - s->arena_len = 0; - s->arena_dead = 0; - } - bool changed = all; term_hist_prepare_rows(&t->hist, cols); - if (!term_hist_view_sync(&t->view, &t->hist, cols)) { - invalidate(s); - return false; - } + if (!term_hist_view_sync(&t->view, &t->hist, cols)) + goto done; term_hist_view_refine(&t->view, &t->hist, cols, rows); - uint64_t stack_keys[256]; - uint64_t *new_keys = rows <= 256 ? stack_keys : malloc((size_t)rows * sizeof(uint64_t)); - if (!new_keys) { - invalidate(s); - return false; - } for (int y = 0; y < rows; y++) new_keys[y] = term_screen_view_row_key(t, y); - int scrolled = 0; if (!all && rows > 1) { int cand_k = 0; /* Find candidate scroll offset k by checking where top rows moved. */ @@ -219,87 +295,38 @@ static bool term_snapshot_update_inner(struct term_snapshot *s, struct term *t) } } - if (scrolled > 0) { - int k = scrolled; - for (int y = 0; y < k; y++) - s->arena_dead += s->row_clusters[y]; - memmove(s->cells, s->cells + (size_t)k * cols, - (size_t)(rows - k) * cols * sizeof(struct term_cell)); - memmove(s->row_clusters, s->row_clusters + k, - (size_t)(rows - k) * sizeof(uint32_t)); - memmove(s->keys, s->keys + k, - (size_t)(rows - k) * sizeof(uint64_t)); - memset(s->keys + (rows - k), 0, (size_t)k * sizeof(uint64_t)); - memset(s->row_clusters + (rows - k), 0, (size_t)k * sizeof(uint32_t)); - } - + /* Arena words for the rows that apply() converts. Count them before + * anything changes, then grow the arena once so apply() cannot fail. */ + need = all ? 0 : s->arena_len; for (int y = 0; y < rows; y++) { - uint64_t key = new_keys[y]; - if (!all && s->keys[y] == key) + uint64_t kept = s->keys[y]; + if (scrolled) + kept = y + scrolled < rows ? s->keys[y + scrolled] : 0; + if (!all && kept == new_keys[y]) continue; - if (!all) - s->arena_dead += s->row_clusters[y]; - const struct cell *src = term_screen_view_row(t, y); - bool converted = true; - s->row_clusters[y] = convert_row(s, t->gstore, s->cells + (size_t)y * cols, - src, cols, &converted); - if (!converted) { - if (new_keys != stack_keys) - free(new_keys); - invalidate(s); - return false; - } - s->keys[y] = key; - changed = true; - } - if (new_keys != stack_keys) - free(new_keys); - - int stack[2][256]; - int *c0 = rows <= 256 ? stack[0] : malloc((size_t)rows * 2 * sizeof(int)); - if (!c0) { - invalidate(s); - return false; + need += row_words(t->gstore, term_screen_view_row(t, y), cols); } - int *c1 = rows <= 256 ? stack[1] : c0 + rows; - term_selection_view_spans(t, c0, c1); - for (int y = 0; y < rows; y++) { - struct term_span span = c1[y] >= c0[y] ? (struct term_span){ c0[y], c1[y] + 1 } - : (struct term_span){ 0 }; - if (all || span.begin != s->selection[y].begin || span.end != s->selection[y].end) - changed = true; - s->selection[y] = span; + /* Offsets must fit below TERM_TEXT_CLUSTER. */ + if (need > TERM_TEXT_CLUSTER) + goto done; + if (need > s->arena_cap) { + uint32_t cap = term_grow_cap32(s->arena_cap ? s->arena_cap * 2 : 256, (uint32_t)need); + uint32_t *a = realloc(s->arena, cap * sizeof(*a)); + if (!a) + goto done; + s->arena = a; + s->arena_cap = cap; } - if (c0 != stack[0]) - free(c0); - struct term_cursor cursor = term_cursor(t); - int view_offset = term_view_offset(t); - cursor.pos.y += view_offset; - if (cursor.pos.y >= rows) - cursor.visible = false; - const struct term_cursor *old = &s->view.cursor; - if (cursor.pos.x != old->pos.x || cursor.pos.y != old->pos.y || - cursor.style != old->style || cursor.visible != old->visible || - cursor.blink != old->blink || - s->view.scroll_offset != view_offset || - s->view.reverse_video != t->reverse_video) - changed = true; - if (all || s->view.palette_version != t->palette.version) { - memcpy(s->palette, t->palette.colors, sizeof(s->palette)); - s->view.palette_version = t->palette.version; - changed = true; - } + term_selection_view_spans(t, c0, c1); + p.all = all; + p.scrolled = scrolled; + apply(s, t, &p); + ok = true; - s->source = t->serial; - s->view.size = (struct term_vector2i){ .x = cols, .y = rows }; - s->view.cursor = cursor; - s->view.scroll_offset = view_offset; - s->view.scrolled = scrolled; - s->view.reverse_video = t->reverse_video; - if (changed || scrolled) - s->view.version++; - return true; +done: + free(heap); + return ok; } bool term_snapshot_update(struct term_snapshot *s, struct term *t) diff --git a/tests/correctness_probe.c b/tests/correctness_probe.c index a7c15df..9ac74ca 100644 --- a/tests/correctness_probe.c +++ b/tests/correctness_probe.c @@ -36,12 +36,11 @@ int main(void) fail_realloc = 1; bool ok = term_snapshot_update(s, t); uint32_t cps[16]; - const struct term_cell *row = term_snapshot_row(s, 0); - int n = row ? term_snapshot_cluster(s, row[0].text, cps, 16) : 0; - printf("snapshot arena allocation failure: update=%d cluster_length=%d (expected false and empty)\n", ok, n); - failed |= ok || term_snapshot_view(s)->size.x != 0 || term_snapshot_view(s)->size.y != 0; + printf("snapshot arena allocation failure: update=%d size=%dx%d (expected false and the previous 8x3 view)\n", + ok, term_snapshot_view(s)->size.x, term_snapshot_view(s)->size.y); + failed |= ok || term_snapshot_view(s)->size.x != 8 || term_snapshot_view(s)->size.y != 3; assert(term_snapshot_update(s, t)); - n = term_snapshot_cluster(s, term_snapshot_row(s, 0)[0].text, cps, 16); + int n = term_snapshot_cluster(s, term_snapshot_row(s, 0)[0].text, cps, 16); printf("retry after allocation recovers: cluster_length=%d (expected 2)\n", n); failed |= n != 2 || cps[0] != 'e' || cps[1] != 0x301; term_snapshot_free(s); 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; } |
