summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-15 11:31:50 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-15 11:31:50 +0300
commitacd338ea33de36d223cc6d301712eed4a9501e6c (patch)
treed93c46a97e2ed4e8a86e3402de5f847cdc144ec8 /tests
parentf97c3a585e2efc1d0a4bc10e6ac04b96ee6428ea (diff)
Fix snapshot storage and allocation failure correctness
Account for cluster records discarded by snapshot scrolling, propagate snapshot allocation failures with an empty retryable view, and allocate resized screens before mutating visible state. Add bounded-memory and allocation-failure regressions, and update the saved diagnostic and audit report.
Diffstat (limited to 'tests')
-rw-r--r--tests/correctness_probe.c13
-rw-r--r--tests/meson.build12
-rw-r--r--tests/resize_failure_test.c51
-rw-r--r--tests/snapshot_failure_test.c91
-rw-r--r--tests/snapshot_scroll_arena_test.c29
5 files changed, 193 insertions, 3 deletions
diff --git a/tests/correctness_probe.c b/tests/correctness_probe.c
index 0b55211..a7c15df 100644
--- a/tests/correctness_probe.c
+++ b/tests/correctness_probe.c
@@ -26,6 +26,7 @@ void *__wrap_realloc(void *ptr, size_t size)
int main(void)
{
+ bool failed = false;
struct term_options options = { .size = { 8, 3 }, .scrollback = 5 };
struct term *t = term_new(&options);
struct term_snapshot *s = term_snapshot_new();
@@ -35,11 +36,14 @@ int main(void)
fail_realloc = 1;
bool ok = term_snapshot_update(s, t);
uint32_t cps[16];
- int n = term_snapshot_cluster(s, term_snapshot_row(s, 0)[0].text, cps, 16);
- printf("snapshot arena allocation failure: update=%d cluster_length=%d (expected false, or length 2)\n", ok, n);
+ 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;
assert(term_snapshot_update(s, t));
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);
term_free(t);
@@ -55,6 +59,7 @@ int main(void)
printf("after %d lines: arena_words=%u dead_words=%u history_rows=%d\n",
i, s->arena_len, s->arena_dead, term_history_rows(t));
}
+ failed |= s->arena_len >= 8192 || term_history_rows(t) != 5;
term_snapshot_free(s);
term_free(t);
@@ -74,7 +79,9 @@ int main(void)
printf("after failed height shrink: resize=%d height=%d top=%c cursor_y=%d history=%d\n",
ok, term_size(t).y, (char)term_snapshot_row(s, 0)[0].text,
term_cursor(t).pos.y, term_history_rows(t));
+ failed |= ok || term_size(t).y != 4 || term_snapshot_row(s, 0)[0].text != 'A' ||
+ term_cursor(t).pos.y != 3 || term_history_rows(t) != 0;
term_snapshot_free(s);
term_free(t);
- return 0;
+ return failed ? 1 : 0;
}
diff --git a/tests/meson.build b/tests/meson.build
index 4cf559c..69f3c49 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -24,6 +24,7 @@ term_tests = [
['gstore-test', files('gstore_test.c'), [], []],
['hist-test', files('hist_test.c'), [], []],
['history-text-test', files('history_text_test.c'), [thread_dep], []],
+ ['snapshot-scroll-arena-test', files('snapshot_scroll_arena_test.c'), [], []],
['view-test', files('view_test.c'), [], []],
['resize-property-test', files('resize_property_test.c'), [], []],
['term-resize-failure-test', files('resize_failure_test.c'), [dl_dep], ['extlib']],
@@ -105,6 +106,17 @@ test('term-header-test', bash,
args: [files('header_test.sh'), meson.project_source_root()],
suite: 'unit',
)
+if cc.has_link_argument('-Wl,--wrap=realloc') and cc.has_link_argument('-Wl,--wrap=malloc')
+ snapshot_failure_test = executable('snapshot-failure-test', 'snapshot_failure_test.c',
+ c_args: test_c_args,
+ link_args: ['-Wl,--wrap=realloc', '-Wl,--wrap=malloc'],
+ dependencies: [term_internal_dep],
+ )
+ test('snapshot-failure-test', snapshot_failure_test,
+ env: test_env,
+ suite: 'unit',
+ )
+endif
test('term-cross-test', bash,
args: [files('cross_test.sh'), meson.project_source_root()],
suite: 'portability',
diff --git a/tests/resize_failure_test.c b/tests/resize_failure_test.c
index 5d7e63c..fbfa38d 100644
--- a/tests/resize_failure_test.c
+++ b/tests/resize_failure_test.c
@@ -146,6 +146,57 @@ int main(void)
assert(tt_cols(term) == 10);
term_free(term);
+ /* A failed same-width height shrink must not scroll rows into history or
+ * move the cursor. The same resize must succeed when allocation recovers. */
+ term = tt_new(8, 4);
+ assert(term);
+ const char four_rows[] = "A\r\nB\r\nC\r\nD";
+ tt_write(term, four_rows, sizeof four_rows - 1);
+ assert(tt_cursor_y(term) == 3);
+ assert(tt_history(term) == 0);
+
+ fail_next_calloc = true;
+ resized = tt_resize(term, 8, 2);
+ assert(!fail_next_calloc);
+ assert(!resized);
+ assert(tt_cols(term) == 8 && tt_rows(term) == 4);
+ assert(tt_cell(term, 0, 0)->text == 'A');
+ assert(tt_cell(term, 0, 1)->text == 'B');
+ assert(tt_cell(term, 0, 2)->text == 'C');
+ assert(tt_cell(term, 0, 3)->text == 'D');
+ assert(tt_cursor_y(term) == 3);
+ assert(tt_history(term) == 0);
+
+ resized = tt_resize(term, 8, 2);
+ assert(resized);
+ assert(tt_cols(term) == 8 && tt_rows(term) == 2);
+ assert(tt_cell(term, 0, 0)->text == 'C');
+ assert(tt_cell(term, 0, 1)->text == 'D');
+ assert(tt_cursor_y(term) == 1);
+ assert(tt_history(term) == 2);
+ term_free(term);
+
+ /* The alternate screen uses the same allocation-before-scroll path. */
+ term = tt_new(8, 4);
+ assert(term);
+ tt_write(term, "\033[?1049h", 8);
+ tt_write(term, four_rows, sizeof four_rows - 1);
+ assert(term_modes(term).alternate);
+ assert(tt_cursor_y(term) == 3);
+ fail_next_calloc = true;
+ resized = tt_resize(term, 8, 2);
+ assert(!fail_next_calloc);
+ assert(!resized);
+ assert(tt_rows(term) == 4 && tt_cursor_y(term) == 3);
+ assert(tt_cell(term, 0, 0)->text == 'A');
+ assert(tt_cell(term, 0, 3)->text == 'D');
+ resized = tt_resize(term, 8, 2);
+ assert(resized);
+ assert(tt_rows(term) == 2 && tt_cursor_y(term) == 1);
+ assert(tt_cell(term, 0, 0)->text == 'C');
+ assert(tt_cell(term, 0, 1)->text == 'D');
+ term_free(term);
+
puts("lazy screen allocation failures preserve observable terminal state");
return 0;
#endif
diff --git a/tests/snapshot_failure_test.c b/tests/snapshot_failure_test.c
new file mode 100644
index 0000000..4133bfd
--- /dev/null
+++ b/tests/snapshot_failure_test.c
@@ -0,0 +1,91 @@
+#include "term/term.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+static bool fail_next_realloc;
+static int malloc_failure_countdown = -1;
+static bool malloc_was_failed;
+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;
+ }
+ }
+ 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;
+ return NULL;
+ }
+ return __real_realloc(p, size);
+}
+
+int main(void)
+{
+ 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);
+ 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);
+ return 0;
+}
diff --git a/tests/snapshot_scroll_arena_test.c b/tests/snapshot_scroll_arena_test.c
new file mode 100644
index 0000000..7da6507
--- /dev/null
+++ b/tests/snapshot_scroll_arena_test.c
@@ -0,0 +1,29 @@
+/* Include snapshot.c to inspect its private arena accounting. The static
+ * library supplies the terminal implementation; this translation unit's
+ * snapshot symbols satisfy the test's references before the archive is read.
+ */
+#include <assert.h>
+#include "../src/snapshot.c"
+
+int main(void)
+{
+ struct term_options options = { .size = { 8, 24 }, .scrollback = 5 };
+ struct term *t = term_new(&options);
+ struct term_snapshot *s = term_snapshot_new();
+ assert(t && s && term_snapshot_update(s, t));
+
+ const char line[] = "e\xcc\x81\r\n";
+ for (int i = 0; i < 10000; i++) {
+ term_feed(t, line, sizeof(line) - 1, 0);
+ assert(term_snapshot_update(s, t));
+ }
+
+ /* A fixed viewport holds fewer than 24 * 8 cluster records at once.
+ * Dead records are compacted before they can dominate this arena. */
+ assert(term_history_rows(t) == 5);
+ assert(s->arena_len < 8192);
+
+ term_snapshot_free(s);
+ term_free(t);
+ return 0;
+}