summaryrefslogtreecommitdiff
path: root/tests/term_test.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/term_test.c')
-rw-r--r--tests/term_test.c215
1 files changed, 193 insertions, 22 deletions
diff --git a/tests/term_test.c b/tests/term_test.c
index 66dbd96..235e9df 100644
--- a/tests/term_test.c
+++ b/tests/term_test.c
@@ -4,6 +4,7 @@
#include <assert.h>
#include <limits.h>
#include <pthread.h>
+#include <sched.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
@@ -1440,6 +1441,55 @@ static void test_snapshot_follows_new_terminal(void)
term_snapshot_free(s);
}
+/* The getters take a NULL snapshot and an out-of-range row like a NULL
+ * terminal: they return the empty value. The view keeps its address. */
+static void test_snapshot_null_and_range(void)
+{
+ uint32_t cps[4] = { 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA };
+ assert(term_snapshot_view(NULL) == NULL);
+ assert(term_snapshot_row(NULL, 0) == NULL);
+ assert(term_snapshot_row_key(NULL, 0) == 0);
+ struct term_span none = term_snapshot_row_selection(NULL, 0);
+ assert(none.begin == 0 && none.end == 0);
+ assert(term_snapshot_cluster(NULL, 'a', cps, 4) == 0);
+ assert(term_snapshot_cluster(NULL, TERM_TEXT_CLUSTER, cps, 4) == 0);
+ assert(cps[0] == 0xAAAA);
+
+ struct term *t = tt_new(4, 2);
+ tt_write(t, "e\xcc\x81", 3);
+ struct term_snapshot *s = term_snapshot_new();
+ assert(term_snapshot_update(s, t));
+
+ const struct term_view *view = term_snapshot_view(s);
+ const uint32_t *palette = view->palette;
+ assert(term_snapshot_update(s, t));
+ assert(term_snapshot_view(s) == view && view->palette == palette);
+
+ assert(term_snapshot_row(s, -1) == NULL);
+ assert(term_snapshot_row(s, 2) == NULL);
+ assert(term_snapshot_row_key(s, -1) == 0);
+ assert(term_snapshot_row_key(s, 2) == 0);
+ struct term_span off = term_snapshot_row_selection(s, 2);
+ assert(off.begin == 0 && off.end == 0);
+
+ /* A cluster cell gives its count with no buffer, and with cap 0 it
+ * writes nothing. */
+ uint32_t text = term_snapshot_row(s, 0)[0].text;
+ assert(text & TERM_TEXT_CLUSTER);
+ assert(term_snapshot_cluster(s, text, NULL, 0) == 2);
+ assert(term_snapshot_cluster(s, text, cps, 0) == 2);
+ assert(cps[0] == 0xAAAA);
+ assert(term_snapshot_cluster(s, text, NULL, 4) == 2);
+ /* A plain code point is one code point, and cap 0 writes nothing. */
+ assert(term_snapshot_cluster(s, 'a', NULL, 0) == 1);
+ assert(term_snapshot_cluster(s, 'a', cps, 0) == 1);
+ assert(cps[0] == 0xAAAA);
+ assert(term_snapshot_cluster(s, 'a', cps, 4) == 1 && cps[0] == 'a');
+
+ term_snapshot_free(s);
+ term_free(t);
+}
+
static void test_string_payloads(void)
{
static const char *prefixes[] = {"\033]0;", "\033]7;file://host/", "\033P"};
@@ -1485,6 +1535,32 @@ static void test_string_payloads(void)
}
}
+/* term_new() refuses a name that a DCS reply could echo unsafely. */
+static void test_host_name_rejected(void)
+{
+ static const char *const bad[] = {
+ "\033x", "a\tb", "\177", "\300", "a\nb",
+ };
+ char longest[65];
+ memset(longest, 'a', 64);
+ longest[64] = 0;
+ char fits[64];
+ memset(fits, 'b', 63);
+ fits[63] = 0;
+
+ for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
+ struct term_options opt = { .size = { .x = 10, .y = 3 }, .name = bad[i] };
+ assert(!term_new(&opt));
+ }
+ struct term_options opt = { .size = { .x = 10, .y = 3 }, .name = longest };
+ assert(!term_new(&opt));
+
+ opt.name = fits;
+ struct term *t = term_new(&opt);
+ assert(t);
+ term_free(t);
+}
+
static void test_newline_policy_survives_reset(void)
{
static const char *const resets[] = { "\033c", "\033[!p", "\033[20l" };
@@ -1762,21 +1838,22 @@ struct concurrent_worker_args {
_Atomic bool done;
};
+static const char concurrent_stream[] =
+ "The quick brown fox jumps over the lazy dog.\r\n"
+ "\033[31mRed text\033[0m and \033[32mgreen text\033[0m.\r\n"
+ "Some unicode: 漢字, ひらがな, and emojis: 😀🚀🎉\r\n"
+ "\033[2J\033[HCleared and reset\r\n"
+ "Line 1\r\nLine 2\r\nLine 3\r\nLine 4\r\nLine 5\r\n"
+ "\033]7;file:///tmp/dir\007\033]22;pointer\007"
+ "\033]8;;https://example.com\007link\033]8;;\007\r\n";
+
static void *concurrent_feed_worker(void *arg)
{
struct concurrent_worker_args *w = arg;
- static const char stream[] =
- "The quick brown fox jumps over the lazy dog.\r\n"
- "\033[31mRed text\033[0m and \033[32mgreen text\033[0m.\r\n"
- "Some unicode: 漢字, ひらがな, and emojis: 😀🚀🎉\r\n"
- "\033[2J\033[HCleared and reset\r\n"
- "Line 1\r\nLine 2\r\nLine 3\r\nLine 4\r\nLine 5\r\n"
- "\033]7;file:///tmp/dir\007\033]22;pointer\007"
- "\033]8;;https://example.com\007link\033]8;;\007\r\n";
- size_t len = strlen(stream);
+ size_t len = strlen(concurrent_stream);
for (int i = 0; i < 2000; i++) {
- term_feed(w->t, stream, len, 0);
+ term_feed(w->t, concurrent_stream, len, 0);
}
atomic_store(&w->done, true);
return NULL;
@@ -1784,10 +1861,11 @@ static void *concurrent_feed_worker(void *arg)
static void test_concurrent_feed_and_snapshot(void)
{
- struct term *t = term_new(&(struct term_options){
+ struct term_options opt = {
.size = { .x = 80, .y = 24 },
.scrollback = 1000,
- });
+ };
+ struct term *t = term_new(&opt);
assert(t != NULL);
struct concurrent_worker_args w = { .t = t, .done = false };
@@ -1828,13 +1906,104 @@ static void test_concurrent_feed_and_snapshot(void)
bool ok = term_snapshot_update(snap, t);
assert(ok);
+ /* The reader changed only the view and the selection. The screen and
+ * the history must match a terminal that got the same bytes on one
+ * thread. */
+ struct term *ref = term_new(&opt);
+ assert(ref != NULL);
+ for (int i = 0; i < 2000; i++)
+ tt_write(ref, concurrent_stream, strlen(concurrent_stream));
+ assert(tt_cols(t) == tt_cols(ref) && tt_rows(t) == tt_rows(ref));
+ assert(tt_history(t) == tt_history(ref));
+ assert(tt_cursor_x(t) == tt_cursor_x(ref) && tt_cursor_y(t) == tt_cursor_y(ref));
+ char *got = malloc((size_t)tt_cols(t) + 1);
+ char *want = malloc((size_t)tt_cols(ref) + 1);
+ assert(got && want);
+ for (int y = 0; y < tt_rows(t); y++) {
+ row_text(t, y, got);
+ row_text(ref, y, want);
+ assert(!strcmp(got, want));
+ }
+ free(got);
+ free(want);
+ char *text = term_history_text(t, 1000);
+ char *text_ref = term_history_text(ref, 1000);
+ assert(text && text_ref && !strcmp(text, text_ref));
+ term_text_free(text);
+ term_text_free(text_ref);
+
+ term_free(ref);
term_snapshot_free(snap);
term_free(t);
}
+static void test_reserved_options_must_be_zero(void)
+{
+ struct term_options opt = { .size = { .x = 10, .y = 2 } };
+ struct term *t = term_new(&opt);
+ assert(t != NULL);
+ term_free(t);
+
+ size_t words = sizeof(opt.reserved) / sizeof(opt.reserved[0]);
+ for (size_t i = 0; i < words; i++) {
+ opt.reserved[i] = 1;
+ assert(term_new(&opt) == NULL);
+ opt.reserved[i] = 0;
+ }
+ opt.reserved[words - 1] = 0x80000000u;
+ assert(term_new(&opt) == NULL);
+ opt.reserved[words - 1] = 0;
+ t = term_new(&opt);
+ assert(t != NULL);
+ term_free(t);
+}
+
+struct lock_probe {
+ struct term *t;
+ _Atomic bool entered;
+};
+
+static void *lock_probe_worker(void *arg)
+{
+ struct lock_probe *p = arg;
+ term_lock(p->t);
+ atomic_store(&p->entered, true);
+ term_unlock(p->t);
+ return NULL;
+}
+
+/* term_lock() nests on the owning thread. Another thread waits until the
+ * last term_unlock(). */
+static void test_lock_reentrant_across_threads(void)
+{
+ struct term *t = term_new(&(struct term_options){
+ .size = { .x = 80, .y = 24 },
+ });
+ assert(t != NULL);
+
+ term_lock(t);
+ term_lock(t);
+ assert(tt_cols(t) == 80);
+ term_unlock(t);
+
+ struct lock_probe p = { .t = t, .entered = false };
+ pthread_t th;
+ assert(pthread_create(&th, NULL, lock_probe_worker, &p) == 0);
+ for (int i = 0; i < 1000; i++)
+ sched_yield();
+ assert(!atomic_load(&p.entered));
+ term_unlock(t);
+
+ assert(pthread_join(th, NULL) == 0);
+ assert(atomic_load(&p.entered));
+ term_free(t);
+}
+
int main(void)
{
+ test_reserved_options_must_be_zero();
test_concurrent_feed_and_snapshot();
+ test_lock_reentrant_across_threads();
test_grow_past_reservation();
test_skipped_cells_read_blank();
test_csi_text_chunks();
@@ -1859,17 +2028,19 @@ int main(void)
test_alt_shrink_then_flood();
test_alt_erase_scrollback();
test_snapshot_follows_new_terminal();
+ test_snapshot_null_and_range();
test_bulk_rows_survive_edits();
-
- assert(!strcmp(UNICODE_DATA_VERSION, "18.0.0"));
- assert(!strcmp(term_unicode_version(), UNICODE_DATA_VERSION));
- assert(unicode_width('A') == 1);
- assert(unicode_width(0x0301) == 0);
- assert(unicode_width(0x754C) == 2);
- assert(unicode_width(0x1FAE0) == 2);
- assert(unicode_emoji_presentation(0x1FAE0));
- assert(unicode_width(0x1FAEB) == 2);
- assert(unicode_emoji_presentation(0x1FAEB));
+ test_host_name_rejected();
+
+ assert(!strcmp(TERM_UNICODE_DATA_VERSION, "18.0.0"));
+ assert(!strcmp(term_unicode_version(), TERM_UNICODE_DATA_VERSION));
+ assert(term_unicode_width('A') == 1);
+ assert(term_unicode_width(0x0301) == 0);
+ assert(term_unicode_width(0x754C) == 2);
+ assert(term_unicode_width(0x1FAE0) == 2);
+ assert(term_unicode_emoji_presentation(0x1FAE0));
+ assert(term_unicode_width(0x1FAEB) == 2);
+ assert(term_unicode_emoji_presentation(0x1FAEB));
{
/* A Jamo Extended-B vowel draws inside its leading consonant. */
struct term *t = term_new(&(struct term_options){ .size = { .x = 10, .y = 2 } });