vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

test.c (33905B)


      1 /* See LICENSE for copyright details */
      2 #include "test-common.c"
      3 
      4 struct test_result { b32 status; const char *info; };
      5 #define TEST_FN(name) struct test_result name(Term *term, Arena arena)
      6 typedef TEST_FN(Test_Fn);
      7 
      8 #define TESTS                             \
      9 	X(csi_embedded_control)           \
     10 	X(colour_setting)                 \
     11 	X(cursor_at_line_boundary)        \
     12 	X(cursor_tabs)                    \
     13 	X(cursor_tabs_across_boundary)    \
     14 	X(working_ringbuffer)
     15 
     16 /* TODO(rnp): skipped the following wiki pages
     17  * - Cursor Horizontal Tabulation (CHT)
     18  * - Cursor Next Line (CNL)
     19  * - Cursor Previous Line (CPL)
     20  * - Cursor Forward (CUF)
     21  * - Set Left and Right Margins (DECSLRM)
     22  * - Delete Line (DL)
     23  */
     24 
     25 #define GHOSTTY_TESTS                     \
     26 	X(cursor_backwards_tabulation_v1) \
     27 	X(cursor_backwards_tabulation_v2) \
     28 	X(cursor_backwards_tabulation_v3) \
     29 	X(cursor_backwards_tabulation_v4) \
     30 	X(cursor_backwards_v1)            \
     31 	X(cursor_backwards_v2)            \
     32 	X(cursor_backwards_v3)            \
     33 	X(cursor_backwards_v4)            \
     34 	X(cursor_backwards_v5)            \
     35 	X(cursor_backwards_v6)            \
     36 	X(cursor_backwards_v7)            \
     37 	X(cursor_down_v1)                 \
     38 	X(cursor_down_v2)                 \
     39 	X(cursor_down_v3)                 \
     40 	X(cursor_position_v1)             \
     41 	X(cursor_position_v2)             \
     42 	X(cursor_position_v3)             \
     43 	X(cursor_position_v4)             \
     44 	X(cursor_position_v5)             \
     45 	X(cursor_position_v6)             \
     46 	X(cursor_up_v1)                   \
     47 	X(cursor_up_v2)                   \
     48 	X(cursor_up_v3)                   \
     49 	X(delete_characters_v1)           \
     50 	X(delete_characters_v2)           \
     51 	X(delete_characters_v3)           \
     52 	X(delete_characters_v4)           \
     53 	X(delete_characters_v5)           \
     54 	X(set_top_bottom_margins_v1)      \
     55 	X(set_top_bottom_margins_v2)      \
     56 	X(set_top_bottom_margins_v3)      \
     57 	X(set_top_bottom_margins_v4)      \
     58 	X(device_status_report_v1)        \
     59 	X(device_status_report_v2)
     60 
     61 #define X(fn) static TEST_FN(fn);
     62 TESTS
     63 GHOSTTY_TESTS
     64 #undef X
     65 
     66 #define X(fn) fn,
     67 static Test_Fn *tests[] = {
     68 	TESTS
     69 	GHOSTTY_TESTS
     70 };
     71 #undef X
     72 
     73 #define ESC(a) s8("\x1B"#a)
     74 #define CSI(a) ESC([a)
     75 
     76 enum {
     77 	FAILURE     = 0,
     78 	SUCCESS     = 1,
     79 	UNSUPPORTED = 2,
     80 };
     81 
     82 static s8 failure_string     = s8("\x1B[31mFAILURE\x1B[0m\n");
     83 static s8 success_string     = s8("\x1B[32mSUCCESS\x1B[0m\n");
     84 static s8 unsupported_string = s8("\x1B[33mUNSUPPORTED\x1B[0m\n");
     85 
     86 static b32
     87 check_cells_equal(Cell *a, Cell *b)
     88 {
     89 	b32 result = a->cp == b->cp &&
     90 	             a->bg == b->bg &&
     91 	             a->fg == b->fg;
     92 	return result;
     93 }
     94 
     95 static TEST_FN(csi_embedded_control)
     96 {
     97 	struct test_result result = {.info = __FUNCTION__};
     98 
     99 	/* NOTE: print a '1' with default style then start changing the colour,
    100 	 * but backspace within the escape sequence so the cursor is now over the
    101 	 * '1', but then put a newline inside the sequence so that cursor is on
    102 	 * the next line, finally print a ' 2' with the completed colour sequence. */
    103 	launder_static_string(term, s8("1"));
    104 	launder_static_string(term, CSI(48;2;7\b5;63;4\n2m));
    105 	s8 raw = launder_static_string(term, s8(" 2"));
    106 	handle_input(term, arena, raw);
    107 
    108 	#if 0
    109 	dump_csi(&term->csi);
    110 	#endif
    111 
    112 	CellStyle final_style = {
    113 		.bg   = (Colour){.r = 75, .g = 63, .b = 42, .a = 0xFF},
    114 		.fg   = g_colours.data[g_colours.fgidx],
    115 		.attr = ATTR_NULL,
    116 	};
    117 
    118 	Cell c1 = {.cp = '1',
    119 		.bg    = SHADER_PACK_BG(g_colours.data[g_colours.bgidx].rgba, ATTR_NULL),
    120 		.fg    = SHADER_PACK_FG(g_colours.data[g_colours.fgidx].rgba, ATTR_NULL),
    121 	};
    122 	Cell c2 = {.cp = '2',
    123 		.bg    = SHADER_PACK_BG(final_style.bg.rgba, final_style.attr),
    124 		.fg    = SHADER_PACK_FG(final_style.fg.rgba, final_style.attr),
    125 	};
    126 
    127 	result.status  = term->cursor.pos.y == 1 && term->cursor.pos.x == 2;
    128 	result.status &= check_cells_equal(&c1, &term->views[term->view_idx].fb.rows[0][0]);
    129 	result.status &= check_cells_equal(&c2, &term->views[term->view_idx].fb.rows[1][1]);
    130 	/* NOTE: we also want to ensure that we cannot split a line in the middle of a CSI */
    131 	LineBuf *lb    = &term->views[0].lines;
    132 	result.status &= lb->filled == 0 && *lb->buf[lb->widx].start != '2';
    133 
    134 	return result;
    135 }
    136 
    137 static TEST_FN(colour_setting)
    138 {
    139 	struct test_result result = {.info = __FUNCTION__};
    140 
    141 	launder_static_string(term, CSI(8m));
    142 	launder_static_string(term, CSI(4m));
    143 	launder_static_string(term, CSI(9m));
    144 	launder_static_string(term, CSI(24m));
    145 	launder_static_string(term, CSI(33m));
    146 	launder_static_string(term, CSI(48;2;75;63;42m));
    147 	s8 raw = launder_static_string(term, s8("A"));
    148 
    149 	handle_input(term, arena, raw);
    150 
    151 	u32 attr = (ATTR_INVISIBLE|ATTR_STRUCK);
    152 	Cell c = {.cp = 'A',
    153 		.bg   = SHADER_PACK_BG(((Colour){.r = 75, .g = 63, .b = 42, .a = 0xFF}.rgba), attr),
    154 		.fg   = SHADER_PACK_FG(g_colours.data[3].rgba, attr),
    155 	};
    156 	result.status = check_cells_equal(&c, term->views[term->view_idx].fb.rows[0]);
    157 
    158 	return result;
    159 }
    160 
    161 static TEST_FN(cursor_at_line_boundary)
    162 {
    163 	/* NOTE: Test that lines are not split in the middle of utf-8 characters */
    164 	struct test_result result = {.info = __FUNCTION__};
    165 
    166 	s8 long_line = s8alloc(&arena, 8192);
    167 	mem_clear(long_line.data, ' ', long_line.len);
    168 
    169 	/* NOTE: change the cursor state in the middle of the line */
    170 	long_line.data[128 + 0] = 0x1B;
    171 	long_line.data[128 + 1] = '[';
    172 	long_line.data[128 + 2] = '5';
    173 	long_line.data[128 + 3] = 'm';
    174 
    175 	/* NOTE: place a non-ASCII character on the long line edge */
    176 	s8 red_dragon = utf8_encode(0x1F004);
    177 	long_line.data[SPLIT_LONG - 1] = red_dragon.data[0];
    178 	long_line.data[SPLIT_LONG + 0] = red_dragon.data[1];
    179 
    180 	s8 raw = launder_static_string(term, (s8){.len = SPLIT_LONG + 1, .data = long_line.data});
    181 	long_line = consume(long_line, SPLIT_LONG + 1);
    182 
    183 	LineBuf *lb = &term->views[term->view_idx].lines;
    184 	size line_count = lb->filled;
    185 	handle_input(term, arena, raw);
    186 
    187 	/* NOTE: ensure line didn't split on red dragon */
    188 	result.status = term->unprocessed_bytes != 0;
    189 
    190 	long_line.data[0] = red_dragon.data[2];
    191 	long_line.data[1] = red_dragon.data[3];
    192 
    193 	/* NOTE: shove a newline at the end so that the line completes */
    194 	long_line.data[long_line.len - 1] = '\n';
    195 
    196 	#if 0
    197 	long_line.data[SPLIT_LONG + 3] = 0;
    198 	printf("encoded char: %s\n", (char *)(long_line.data + SPLIT_LONG - 1));
    199 	long_line.data[SPLIT_LONG + 3] = ' ';
    200 	#endif
    201 
    202 	raw = launder_static_string(term, long_line);
    203 	handle_input(term, arena, raw);
    204 
    205 	result.status &= line_length(lb->buf) > SPLIT_LONG;
    206 
    207 	return result;
    208 }
    209 
    210 static TEST_FN(cursor_tabs)
    211 {
    212 	struct test_result result = {.info = __FUNCTION__};
    213 
    214 	/* NOTE: first test advancing to a tabstop */
    215 	s8 raw = launder_static_string(term, s8("123\t"));
    216 	handle_input(term, arena, raw);
    217 
    218 	result.status = term->cursor.pos.x == (g_tabstop);
    219 
    220 	/* NOTE: now test negative tabstop movement and tabstop setting */
    221 	launder_static_string(term, s8("12"));
    222 	launder_static_string(term, ESC(H));
    223 	launder_static_string(term, s8("34\t"));
    224 	raw = launder_static_string(term, CSI(2Z));
    225 	handle_input(term, arena, raw);
    226 
    227 	result.status &= term->cursor.pos.x == (g_tabstop);
    228 
    229 	return result;
    230 }
    231 
    232 static TEST_FN(cursor_tabs_across_boundary)
    233 {
    234 	struct test_result result = {.info = __FUNCTION__};
    235 
    236 	/* NOTE: clear tabstops then set one beyond multiple boundaries */
    237 	launder_static_string(term, CSI(3g));
    238 	launder_static_string(term, CSI(1;67H));
    239 	launder_static_string(term, ESC(H));
    240 	launder_static_string(term, CSI(1;1H));
    241 	s8 raw = launder_static_string(term, s8("\t"));
    242 	handle_input(term, arena, raw);
    243 
    244 	result.status = term->cursor.pos.x == 66;
    245 
    246 	/* NOTE: now set one exactly on a boundary */
    247 	launder_static_string(term, CSI(1;34H));
    248 	launder_static_string(term, ESC(H));
    249 	launder_static_string(term, CSI(1;1H));
    250 	raw = launder_static_string(term, s8("\t"));
    251 	handle_input(term, arena, raw);
    252 
    253 	result.status &= term->cursor.pos.x == 33;
    254 
    255 	/* NOTE: now set one right before the previous */
    256 	launder_static_string(term, CSI(1;33H));
    257 	launder_static_string(term, ESC(H));
    258 	launder_static_string(term, CSI(1;1H));
    259 
    260 	raw = launder_static_string(term, s8("\t"));
    261 	handle_input(term, arena, raw);
    262 	result.status &= term->cursor.pos.x == 32;
    263 
    264 	raw = launder_static_string(term, s8("\t"));
    265 	handle_input(term, arena, raw);
    266 	result.status &= term->cursor.pos.x == 33;
    267 
    268 	/* NOTE: ensure backwards tab works */
    269 	launder_static_string(term, CSI(1;34H));
    270 	launder_static_string(term, ESC(0g));
    271 	launder_static_string(term, CSI(1;66H));
    272 	raw = launder_static_string(term, CSI(1Z));
    273 	handle_input(term, arena, raw);
    274 	result.status &= term->cursor.pos.x == 33;
    275 
    276 	return result;
    277 }
    278 
    279 static TEST_FN(working_ringbuffer)
    280 {
    281 	struct test_result result = {.info = __FUNCTION__};
    282 	RingBuf *rb = &term->views[term->view_idx].log;
    283 	rb->buf[0]  = 0xFE;
    284 	result.status = (rb->buf[0] == rb->buf[ rb->cap]) &&
    285 	                (rb->buf[0] == rb->buf[-rb->cap]);
    286 	return result;
    287 }
    288 
    289 /***********************************************/
    290 /* GHOSTTY TESTS (https://ghostty.org/docs/vt) */
    291 /***********************************************/
    292 
    293 /* NOTE: CBT V-1: Left Beyond First Column */
    294 static TEST_FN(cursor_backwards_tabulation_v1)
    295 {
    296 	struct test_result result = {.info = __FUNCTION__};
    297 
    298 	launder_static_string(term, s8("\n"));
    299 	launder_static_string(term, CSI(?5W));
    300 	launder_static_string(term, CSI(10Z));
    301 	s8 raw = launder_static_string(term, s8("A"));
    302 	handle_input(term, arena, raw);
    303 
    304 	result.status  = term->cursor.pos.x == 1 && term->cursor.pos.y == 1;
    305 	result.status &= term->views[term->view_idx].fb.rows[1][0].cp == 'A';
    306 
    307 	return result;
    308 }
    309 
    310 /* NOTE: CBT V-2: Left Starting After Tab Stop */
    311 static TEST_FN(cursor_backwards_tabulation_v2)
    312 {
    313 	struct test_result result = {.info = __FUNCTION__};
    314 
    315 	launder_static_string(term, CSI(?5W));
    316 	launder_static_string(term, CSI(1;10H));
    317 	launder_static_string(term, s8("X"));
    318 	launder_static_string(term, CSI(Z));
    319 	s8 raw = launder_static_string(term, s8("A"));
    320 	handle_input(term, arena, raw);
    321 
    322 	result.status  = term->views[term->view_idx].fb.rows[0][8].cp == 'A';
    323 	result.status &= term->views[term->view_idx].fb.rows[0][9].cp == 'X';
    324 
    325 	return result;
    326 }
    327 
    328 /* NOTE: CBT V-3: Left Starting on Tabstop */
    329 static TEST_FN(cursor_backwards_tabulation_v3)
    330 {
    331 	struct test_result result = {.info = __FUNCTION__};
    332 
    333 	launder_static_string(term, CSI(?5W));
    334 	launder_static_string(term, CSI(1;9H));
    335 	launder_static_string(term, s8("X"));
    336 	launder_static_string(term, CSI(1;9H));
    337 	launder_static_string(term, CSI(Z));
    338 	s8 raw = launder_static_string(term, s8("A"));
    339 	handle_input(term, arena, raw);
    340 
    341 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'A';
    342 	result.status &= term->views[term->view_idx].fb.rows[0][8].cp == 'X';
    343 	result.status &= term->views[term->view_idx].fb.rows[0][9].cp == ' ';
    344 
    345 	return result;
    346 }
    347 
    348 /* NOTE: CBT V-4: Left Margin with Origin Mode */
    349 static TEST_FN(cursor_backwards_tabulation_v4)
    350 {
    351 	struct test_result result = {.info = __FUNCTION__};
    352 	result.status = UNSUPPORTED;
    353 
    354 	#if 0
    355 	launder_static_string(term, CSI(1;1H));
    356 	launder_static_string(term, CSI(0J));
    357 	launder_static_string(term, CSI(?5W));
    358 	launder_static_string(term, CSI(?6h));
    359 	launder_static_string(term, CSI(?69h));
    360 	launder_static_string(term, CSI(3;6s));
    361 	launder_static_string(term, CSI(1;2H));
    362 	launder_static_string(term, s8("X"));
    363 	launder_static_string(term, CSI(Z));
    364 	s8 raw = launder_static_string(term, s8("A"));
    365 	handle_input(term, arena, raw);
    366 
    367 	result.status  = term->views[term->view_idx].fb.rows[0][2].cp == 'A';
    368 	result.status &= term->views[term->view_idx].fb.rows[0][3].cp == 'X';
    369 	#endif
    370 
    371 	return result;
    372 }
    373 
    374 /* NOTE: CUB V-1: Pending Wrap is Unset */
    375 static TEST_FN(cursor_backwards_v1)
    376 {
    377 	struct test_result result = {.info = __FUNCTION__};
    378 
    379 	u8 buffer_store[32];
    380 	Stream buffer = {.buf = buffer_store, .cap = sizeof(buffer_store)};
    381 	stream_push_s8(&buffer, s8("\x1B["));
    382 	stream_push_u64(&buffer, term->size.w);
    383 	stream_push_byte(&buffer, 'G');
    384 	launder_static_string(term, stream_to_s8(&buffer));
    385 
    386 	launder_static_string(term, s8("A"));
    387 	launder_static_string(term, CSI(D));
    388 	s8 raw = launder_static_string(term, s8("XYZ"));
    389 	handle_input(term, arena, raw);
    390 
    391 	result.status  = term->views[term->view_idx].fb.rows[0][term->size.w - 2].cp == 'X';
    392 	result.status &= term->views[term->view_idx].fb.rows[0][term->size.w - 1].cp == 'Y';
    393 	result.status &= term->views[term->view_idx].fb.rows[1][0].cp == 'Z';
    394 	result.status &= term->cursor.pos.y == 1 && term->cursor.pos.x == 1;
    395 
    396 	return result;
    397 }
    398 
    399 /* NOTE: CUB V-2: Leftmost Boundary with Reverse Wrap Disabled */
    400 static TEST_FN(cursor_backwards_v2)
    401 {
    402 	struct test_result result = {.info = __FUNCTION__};
    403 
    404 	launder_static_string(term, CSI(?45l));
    405 	launder_static_string(term, s8("A\r\n"));
    406 	launder_static_string(term, CSI(10D));
    407 	s8 raw = launder_static_string(term, s8("B"));
    408 	handle_input(term, arena, raw);
    409 
    410 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'A';
    411 	result.status &= term->views[term->view_idx].fb.rows[1][0].cp == 'B';
    412 	result.status &= term->cursor.pos.y == 1 && term->cursor.pos.x == 1;
    413 
    414 	return result;
    415 }
    416 
    417 /* NOTE: CUB V-3: Reverse Wrap */
    418 static TEST_FN(cursor_backwards_v3)
    419 {
    420 	struct test_result result = {.info = __FUNCTION__};
    421 	result.status = UNSUPPORTED;
    422 
    423 	#if 0
    424 	u8 buffer_store[32];
    425 	Stream buffer = {.buf = buffer_store, .cap = sizeof(buffer_store)};
    426 	stream_push_s8(&buffer, s8("\x1B["));
    427 	stream_push_u64(&buffer, term->size.w);
    428 	stream_push_byte(&buffer, 'G');
    429 
    430 	launder_static_string(term, CSI(?7h));
    431 	launder_static_string(term, CSI(?45h));
    432 	launder_static_string(term, stream_to_s8(&buffer));
    433 	launder_static_string(term, s8("AB"));
    434 	launder_static_string(term, CSI(D));
    435 	s8 raw = launder_static_string(term, s8("X"));
    436 	handle_input(term, arena, raw);
    437 
    438 	result.status  = term->views[term->view_idx].fb.rows[0][term->size.w - 1].cp == 'X';
    439 	result.status &= term->views[term->view_idx].fb.rows[1][0].cp == 'B';
    440 	result.status &= term->cursor.pos.y == 0 && term->cursor.pos.x == term->size.w - 1;
    441 	result.status &= (term->cursor.state & CURSOR_WRAP_NEXT) != 0;
    442 	#endif
    443 
    444 	return result;
    445 }
    446 
    447 /* NOTE: CUB V-4: Extended Reverse Wrap Single Line */
    448 static TEST_FN(cursor_backwards_v4)
    449 {
    450 	struct test_result result = {.info = __FUNCTION__};
    451 	result.status = UNSUPPORTED;
    452 
    453 	#if 0
    454 	launder_static_string(term, CSI(?7h));
    455 	launder_static_string(term, CSI(?1045h));
    456 	launder_static_string(term, s8("A\r\nB"));
    457 	launder_static_string(term, CSI(2D));
    458 	s8 raw = launder_static_string(term, s8("X"));
    459 	handle_input(term, arena, raw);
    460 
    461 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'A';
    462 	result.status &= term->views[term->view_idx].fb.rows[1][0].cp == 'B';
    463 	result.status &= term->views[term->view_idx].fb.rows[0][term->size.w - 1].cp == 'X';
    464 	result.status &= term->cursor.pos.y == 0 && term->cursor.pos.x == term->size.w - 1;
    465 	result.status &= (term->cursor.state & CURSOR_WRAP_NEXT) != 0;
    466 	#endif
    467 
    468 	return result;
    469 }
    470 
    471 /* NOTE: CUB V-5: Extended Reverse Wrap Wraps to Bottom */
    472 static TEST_FN(cursor_backwards_v5)
    473 {
    474 	struct test_result result = {.info = __FUNCTION__};
    475 	result.status = UNSUPPORTED;
    476 
    477 	#if 0
    478 	u8 buffer_store[32];
    479 	Stream buffer = {.buf = buffer_store, .cap = sizeof(buffer_store)};
    480 	stream_push_s8(&buffer, s8("\x1B["));
    481 	stream_push_u64(&buffer, term->size.w);
    482 	stream_push_byte(&buffer, 'D');
    483 
    484 	launder_static_string(term, CSI(?7h));
    485 	launder_static_string(term, CSI(?45h));
    486 	launder_static_string(term, CSI(1;3r));
    487 	launder_static_string(term, s8("A\r\nB"));
    488 	launder_static_string(term, CSI(D));
    489 	launder_static_string(term, stream_to_s8(&buffer));
    490 	launder_static_string(term, CSI(D));
    491 	s8 raw = launder_static_string(term, s8("X"));
    492 	handle_input(term, arena, raw);
    493 
    494 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'A';
    495 	result.status &= term->views[term->view_idx].fb.rows[1][0].cp == 'B';
    496 	result.status &= term->views[term->view_idx].fb.rows[2][term->size.w - 1].cp == 'X';
    497 	result.status &= term->cursor.pos.y == 0 && term->cursor.pos.x == term->size.w - 1;
    498 	result.status &= (term->cursor.state & CURSOR_WRAP_NEXT) != 0;
    499 	#endif
    500 
    501 	return result;
    502 }
    503 
    504 /* NOTE: CUB V-6: Reverse Wrap Outside of Margins */
    505 static TEST_FN(cursor_backwards_v6)
    506 {
    507 	struct test_result result = {.info = __FUNCTION__};
    508 	result.status = UNSUPPORTED;
    509 
    510 	#if 0
    511 	launder_static_string(term, CSI(?45h));
    512 	launder_static_string(term, CSI(3r));
    513 	s8 raw = launder_static_string(term, s8("\bX"));
    514 	handle_input(term, arena, raw);
    515 
    516 	result.status  = term->views[term->view_idx].fb.rows[2][0].cp == 'X';
    517 	result.status &= term->cursor.pos.y == 2 && term->cursor.pos.x == 1;
    518 	#endif
    519 
    520 	return result;
    521 }
    522 
    523 /* NOTE: CUB V-7: Reverse Wrap with Pending Wrap State */
    524 static TEST_FN(cursor_backwards_v7)
    525 {
    526 	struct test_result result = {.info = __FUNCTION__};
    527 	result.status = UNSUPPORTED;
    528 
    529 	#if 0
    530 	u8 buffer_store[32];
    531 	Stream buffer = {.buf = buffer_store, .cap = sizeof(buffer_store)};
    532 	stream_push_s8(&buffer, s8("\x1B["));
    533 	stream_push_u64(&buffer, term->size.w);
    534 	stream_push_byte(&buffer, 'G');
    535 
    536 	launder_static_string(term, CSI(?45h));
    537 	launder_static_string(term, stream_to_s8(&buffer));
    538 	launder_static_string(term, CSI(4D));
    539 	launder_static_string(term, s8("ABCDE"));
    540 	launder_static_string(term, CSI(D));
    541 	s8 raw = launder_static_string(term, s8("X"));
    542 	handle_input(term, arena, raw);
    543 
    544 	result.status  = term->views[term->view_idx].fb.rows[0][term->size.w - 1].cp == 'X';
    545 	result.status &= term->views[term->view_idx].fb.rows[0][term->size.w - 2].cp == 'D';
    546 	result.status &= term->views[term->view_idx].fb.rows[0][term->size.w - 3].cp == 'C';
    547 	result.status &= term->views[term->view_idx].fb.rows[0][term->size.w - 4].cp == 'B';
    548 	result.status &= term->views[term->view_idx].fb.rows[0][term->size.w - 5].cp == 'A';
    549 	result.status &= term->cursor.pos.y == 0 && term->cursor.pos.x == term->size.w - 1;
    550 	result.status &= (term->cursor.state & CURSOR_WRAP_NEXT) != 0;
    551 	#endif
    552 
    553 	return result;
    554 }
    555 
    556 /* NOTE: CUD V-1: Cursor Down */
    557 static TEST_FN(cursor_down_v1)
    558 {
    559 	struct test_result result = {.info = __FUNCTION__};
    560 
    561 	launder_static_string(term, s8("A"));
    562 	launder_static_string(term, CSI(2B));
    563 	s8 raw = launder_static_string(term, s8("X"));
    564 	handle_input(term, arena, raw);
    565 
    566 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'A';
    567 	result.status &= term->views[term->view_idx].fb.rows[2][1].cp == 'X';
    568 	result.status &= term->cursor.pos.y == 2 && term->cursor.pos.x == 2;
    569 
    570 	return result;
    571 }
    572 
    573 /* NOTE: CUD V-2: Cursor Down Above Bottom Margin */
    574 static TEST_FN(cursor_down_v2)
    575 {
    576 	struct test_result result = {.info = __FUNCTION__};
    577 
    578 	launder_static_string(term, CSI(1;3r));
    579 	launder_static_string(term, s8("A"));
    580 	launder_static_string(term, CSI(5B));
    581 	s8 raw = launder_static_string(term, s8("X"));
    582 	handle_input(term, arena, raw);
    583 
    584 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'A';
    585 	result.status &= term->views[term->view_idx].fb.rows[2][1].cp == 'X';
    586 	result.status &= term->cursor.pos.y == 2 && term->cursor.pos.x == 2;
    587 
    588 	return result;
    589 }
    590 
    591 /* NOTE: CUD V-3: Cursor Down Below Bottom Margin */
    592 static TEST_FN(cursor_down_v3)
    593 {
    594 	struct test_result result = {.info = __FUNCTION__};
    595 
    596 	launder_static_string(term, CSI(1;3r));
    597 	launder_static_string(term, s8("A"));
    598 	launder_static_string(term, CSI(4;1H));
    599 	launder_static_string(term, CSI(5B));
    600 	s8 raw = launder_static_string(term, s8("X"));
    601 	handle_input(term, arena, raw);
    602 
    603 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'A';
    604 	result.status &= term->views[term->view_idx].fb.rows[8][0].cp == 'X';
    605 	result.status &= term->cursor.pos.y == 8 && term->cursor.pos.x == 1;
    606 
    607 	return result;
    608 }
    609 
    610 /* NOTE: CUP V-1: Normal Usage */
    611 static TEST_FN(cursor_position_v1)
    612 {
    613 	struct test_result result = {.info = __FUNCTION__};
    614 
    615 	launder_static_string(term, CSI(2;3H));
    616 	s8 raw = launder_static_string(term, s8("A"));
    617 	handle_input(term, arena, raw);
    618 
    619 	result.status  = term->views[term->view_idx].fb.rows[1][2].cp == 'A';
    620 	result.status &= term->cursor.pos.y == 1 && term->cursor.pos.x == 3;
    621 
    622 	return result;
    623 }
    624 
    625 /* NOTE: CUP V-2: Off the Screen */
    626 static TEST_FN(cursor_position_v2)
    627 {
    628 	struct test_result result = {.info = __FUNCTION__};
    629 
    630 	launder_static_string(term, CSI(500;500H));
    631 	s8 raw = launder_static_string(term, s8("A"));
    632 	handle_input(term, arena, raw);
    633 
    634 	result.status  = term->views[term->view_idx].fb.rows[term->size.h - 1][term->size.w - 1].cp == 'A';
    635 	result.status &= term->cursor.pos.y == (term->size.h - 1);
    636 	result.status &= term->cursor.pos.x == (term->size.w - 1);
    637 	result.status &= (term->cursor.state & CURSOR_WRAP_NEXT) != 0;
    638 
    639 	return result;
    640 }
    641 
    642 /* NOTE: CUP V-3: Relative to Origin */
    643 static TEST_FN(cursor_position_v3)
    644 {
    645 	struct test_result result = {.info = __FUNCTION__};
    646 
    647 	launder_static_string(term, CSI(2;3r));
    648 	launder_static_string(term, CSI(?6h));
    649 	launder_static_string(term, CSI(1;1H));
    650 	s8 raw = launder_static_string(term, s8("X"));
    651 	handle_input(term, arena, raw);
    652 
    653 	result.status = term->views[term->view_idx].fb.rows[1][0].cp == 'X';
    654 
    655 	return result;
    656 }
    657 
    658 /* NOTE: CUP V-4: Relative to Origin with Left/Right Margins */
    659 static TEST_FN(cursor_position_v4)
    660 {
    661 	struct test_result result = {.info = __FUNCTION__};
    662 	result.status = UNSUPPORTED;
    663 
    664 	#if 0
    665 	launder_static_string(term, CSI(?69h));
    666 	launder_static_string(term, CSI(3;5s));
    667 	launder_static_string(term, CSI(2;3r));
    668 	launder_static_string(term, CSI(?6h));
    669 	launder_static_string(term, CSI(1;1H));
    670 	s8 raw = launder_static_string(term, s8("X"));
    671 	handle_input(term, arena, raw);
    672 
    673 	result.status = term->views[term->view_idx].fb.rows[1][2].cp == 'X';
    674 	#endif
    675 
    676 	return result;
    677 }
    678 
    679 /* NOTE: CUP V-5: Limits with Scroll Region and Origin Mode */
    680 static TEST_FN(cursor_position_v5)
    681 {
    682 	struct test_result result = {.info = __FUNCTION__};
    683 	result.status = UNSUPPORTED;
    684 
    685 	#if 0
    686 	launder_static_string(term, CSI(?69h));
    687 	launder_static_string(term, CSI(3;5s));
    688 	launder_static_string(term, CSI(2;3r));
    689 	launder_static_string(term, CSI(?6h));
    690 	launder_static_string(term, CSI(500;500H));
    691 	s8 raw = launder_static_string(term, s8("X"));
    692 	handle_input(term, arena, raw);
    693 
    694 	result.status = term->views[term->view_idx].fb.rows[2][4].cp == 'X';
    695 	#endif
    696 
    697 	return result;
    698 }
    699 
    700 /* NOTE: CUP V-6: Pending Wrap is Unset */
    701 static TEST_FN(cursor_position_v6)
    702 {
    703 	struct test_result result = {.info = __FUNCTION__};
    704 
    705 	u8 buffer_store[32];
    706 	Stream buffer = {.buf = buffer_store, .cap = sizeof(buffer_store)};
    707 	stream_push_s8(&buffer, s8("\x1B["));
    708 	stream_push_u64(&buffer, term->size.w);
    709 	stream_push_byte(&buffer, 'G');
    710 
    711 	launder_static_string(term, stream_to_s8(&buffer));
    712 	launder_static_string(term, s8("A"));
    713 	launder_static_string(term, CSI(1;1H));
    714 	s8 raw = launder_static_string(term, s8("X"));
    715 	handle_input(term, arena, raw);
    716 
    717 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'X';
    718 	result.status &= term->views[term->view_idx].fb.rows[0][term->size.w - 1].cp == 'A';
    719 	result.status &= term->cursor.pos.y == 0 && term->cursor.pos.x == 1;
    720 
    721 	return result;
    722 }
    723 
    724 /* NOTE: CUU V-1: Cursor Up */
    725 static TEST_FN(cursor_up_v1)
    726 {
    727 	struct test_result result = {.info = __FUNCTION__};
    728 
    729 	launder_static_string(term, CSI(3;1H));
    730 	launder_static_string(term, s8("A"));
    731 	launder_static_string(term, CSI(2A));
    732 	s8 raw = launder_static_string(term, s8("X"));
    733 	handle_input(term, arena, raw);
    734 
    735 	result.status  = term->views[term->view_idx].fb.rows[0][1].cp == 'X';
    736 	result.status &= term->views[term->view_idx].fb.rows[2][0].cp == 'A';
    737 	result.status &= term->cursor.pos.y == 0 && term->cursor.pos.x == 2;
    738 
    739 	return result;
    740 }
    741 
    742 /* NOTE: CUU V-2: Cursor Up Below Top Margin */
    743 static TEST_FN(cursor_up_v2)
    744 {
    745 	struct test_result result = {.info = __FUNCTION__};
    746 
    747 	launder_static_string(term, CSI(2;4r));
    748 	launder_static_string(term, CSI(3;1H));
    749 	launder_static_string(term, s8("A"));
    750 	launder_static_string(term, CSI(5A));
    751 	s8 raw = launder_static_string(term, s8("X"));
    752 	handle_input(term, arena, raw);
    753 
    754 	result.status  = term->views[term->view_idx].fb.rows[1][1].cp == 'X';
    755 	result.status &= term->views[term->view_idx].fb.rows[2][0].cp == 'A';
    756 	result.status &= term->cursor.pos.y == 1 && term->cursor.pos.x == 2;
    757 
    758 	return result;
    759 }
    760 
    761 /* NOTE: CUU V-3: Cursor Up Above Top Margin */
    762 static TEST_FN(cursor_up_v3)
    763 {
    764 	struct test_result result = {.info = __FUNCTION__};
    765 
    766 	launder_static_string(term, CSI(3;5r));
    767 	launder_static_string(term, CSI(3;1H));
    768 	launder_static_string(term, s8("A"));
    769 	launder_static_string(term, CSI(2;1H));
    770 	launder_static_string(term, CSI(5A));
    771 	s8 raw = launder_static_string(term, s8("X"));
    772 	handle_input(term, arena, raw);
    773 
    774 	result.status  = term->views[term->view_idx].fb.rows[0][0].cp == 'X';
    775 	result.status &= term->views[term->view_idx].fb.rows[2][0].cp == 'A';
    776 	result.status &= term->cursor.pos.y == 0 && term->cursor.pos.x == 1;
    777 
    778 	return result;
    779 }
    780 
    781 
    782 /* NOTE: DCH V-1: Simple Delete Character */
    783 static TEST_FN(delete_characters_v1)
    784 {
    785 	struct test_result result = {.info = __FUNCTION__};
    786 
    787 	launder_static_string(term, s8("ABC123"));
    788 	launder_static_string(term, CSI(3G));
    789 	s8 raw = launder_static_string(term, CSI(2P));
    790 	handle_input(term, arena, raw);
    791 
    792 	Row row        = term->views[term->view_idx].fb.rows[0];
    793 	result.status  = row[0].cp == 'A';
    794 	result.status &= row[1].cp == 'B';
    795 	result.status &= row[2].cp == '2';
    796 	result.status &= row[3].cp == '3';
    797 	result.status &= row[4].cp == ' ';
    798 	result.status &= row[5].cp == ' ';
    799 
    800 	return result;
    801 }
    802 
    803 /* NOTE: DCH V-2: SGR State */
    804 static TEST_FN(delete_characters_v2)
    805 {
    806 	struct test_result result = {.info = __FUNCTION__};
    807 
    808 	launder_static_string(term, s8("ABC123"));
    809 	launder_static_string(term, CSI(3G));
    810 	launder_static_string(term, CSI(41m));
    811 	s8 raw = launder_static_string(term, CSI(2P));
    812 	handle_input(term, arena, raw);
    813 
    814 	Cell cell = {.cp = ' ',
    815 		.bg    = SHADER_PACK_BG(g_colours.data[1].rgba,               ATTR_NULL),
    816 		.fg    = SHADER_PACK_FG(g_colours.data[g_colours.fgidx].rgba, ATTR_NULL),
    817 	};
    818 
    819 	Row row        = term->views[term->view_idx].fb.rows[0];
    820 	result.status  = row[0].cp == 'A';
    821 	result.status &= row[1].cp == 'B';
    822 	result.status &= row[2].cp == '2';
    823 	result.status &= row[3].cp == '3';
    824 	result.status &= check_cells_equal(&cell, &row[term->size.w - 2]);
    825 	result.status &= check_cells_equal(&cell, &row[term->size.w - 1]);
    826 
    827 	return result;
    828 }
    829 
    830 /* NOTE: DCH V-3: Outside Left/Right Scroll Region */
    831 static TEST_FN(delete_characters_v3)
    832 {
    833 	struct test_result result = {.info = __FUNCTION__};
    834 	result.status = UNSUPPORTED;
    835 
    836 	#if 0
    837 	launder_static_string(term, s8("ABC123"));
    838 	launder_static_string(term, CSI(?69h));
    839 	launder_static_string(term, CSI(3;5s));
    840 	launder_static_string(term, CSI(2G));
    841 	s8 raw = launder_static_string(term, CSI(P));
    842 	handle_input(term, arena, raw);
    843 
    844 	Row row        = term->views[term->view_idx].fb.rows[0];
    845 	result.status  = row[0].cp == 'A';
    846 	result.status &= row[1].cp == 'B';
    847 	result.status &= row[2].cp == 'C';
    848 	result.status &= row[3].cp == '1';
    849 	result.status &= row[4].cp == '2';
    850 	result.status &= row[5].cp == '3';
    851 	#endif
    852 
    853 	return result;
    854 }
    855 
    856 /* NOTE: DCH V-4: Inside Left/Right Scroll Region */
    857 static TEST_FN(delete_characters_v4)
    858 {
    859 	struct test_result result = {.info = __FUNCTION__};
    860 	result.status = UNSUPPORTED;
    861 
    862 	#if 0
    863 	launder_static_string(term, s8("ABC123"));
    864 	launder_static_string(term, CSI(?69h));
    865 	launder_static_string(term, CSI(3;5s));
    866 	launder_static_string(term, CSI(4G));
    867 	s8 raw = launder_static_string(term, CSI(P));
    868 	handle_input(term, arena, raw);
    869 
    870 	Row row        = term->views[term->view_idx].fb.rows[0];
    871 	result.status  = row[0].cp == 'A';
    872 	result.status &= row[1].cp == 'B';
    873 	result.status &= row[2].cp == 'C';
    874 	result.status &= row[3].cp == '2';
    875 	result.status &= row[4].cp == ' ';
    876 	result.status &= row[5].cp == '3';
    877 	#endif
    878 
    879 	return result;
    880 }
    881 
    882 /* NOTE: DCH V-5: Split Wide Character */
    883 static TEST_FN(delete_characters_v5)
    884 {
    885 	struct test_result result = {.info = __FUNCTION__};
    886 
    887 	launder_static_string(term, s8("Ać©‹123"));
    888 	launder_static_string(term, CSI(3G));
    889 	s8 raw = launder_static_string(term, CSI(P));
    890 	handle_input(term, arena, raw);
    891 
    892 	Row row        = term->views[term->view_idx].fb.rows[0];
    893 	result.status  = row[0].cp == 'A';
    894 	result.status &= row[1].cp == ' ';
    895 	result.status &= row[2].cp == '1';
    896 	result.status &= row[3].cp == '2';
    897 	result.status &= row[4].cp == '3';
    898 
    899 	return result;
    900 }
    901 
    902 /* NOTE: DECSTBM V-1: Full Screen */
    903 static TEST_FN(set_top_bottom_margins_v1)
    904 {
    905 	struct test_result result = {.info = __FUNCTION__};
    906 
    907 	launder_static_string(term, s8("ABC\r\nDEF\r\nGHI\r\n"));
    908 	launder_static_string(term, CSI(r));
    909 	s8 raw = launder_static_string(term, CSI(T));
    910 	handle_input(term, arena, raw);
    911 
    912 	Row *rows      = term->views[term->view_idx].fb.rows;
    913 	result.status  = rows[0][0].cp == ' ';
    914 	result.status &= rows[0][1].cp == ' ';
    915 	result.status &= rows[0][2].cp == ' ';
    916 	result.status &= rows[1][0].cp == 'A';
    917 	result.status &= rows[1][1].cp == 'B';
    918 	result.status &= rows[1][2].cp == 'C';
    919 	result.status &= rows[2][0].cp == 'D';
    920 	result.status &= rows[2][1].cp == 'E';
    921 	result.status &= rows[2][2].cp == 'F';
    922 	result.status &= rows[3][0].cp == 'G';
    923 	result.status &= rows[3][1].cp == 'H';
    924 	result.status &= rows[3][2].cp == 'I';
    925 	result.status &= term->cursor.pos.x == 0 && term->cursor.pos.y == 0;
    926 
    927 	return result;
    928 }
    929 
    930 /* NOTE: DECSTBM V-2: Top Only */
    931 static TEST_FN(set_top_bottom_margins_v2)
    932 {
    933 	struct test_result result = {.info = __FUNCTION__};
    934 
    935 	launder_static_string(term, s8("ABC\r\nDEF\r\nGHI\r\n"));
    936 	launder_static_string(term, CSI(2r));
    937 	s8 raw = launder_static_string(term, CSI(T));
    938 	handle_input(term, arena, raw);
    939 
    940 	Row *rows      = term->views[term->view_idx].fb.rows;
    941 	result.status  = rows[0][0].cp == 'A';
    942 	result.status &= rows[0][1].cp == 'B';
    943 	result.status &= rows[0][2].cp == 'C';
    944 	result.status &= rows[1][0].cp == ' ';
    945 	result.status &= rows[1][1].cp == ' ';
    946 	result.status &= rows[1][2].cp == ' ';
    947 	result.status &= rows[2][0].cp == 'D';
    948 	result.status &= rows[2][1].cp == 'E';
    949 	result.status &= rows[2][2].cp == 'F';
    950 	result.status &= rows[3][0].cp == 'G';
    951 	result.status &= rows[3][1].cp == 'H';
    952 	result.status &= rows[3][2].cp == 'I';
    953 
    954 	return result;
    955 }
    956 
    957 /* NOTE: DECSTBM V-3: Top and Bottom */
    958 static TEST_FN(set_top_bottom_margins_v3)
    959 {
    960 	struct test_result result = {.info = __FUNCTION__};
    961 
    962 	launder_static_string(term, s8("ABC\r\nDEF\r\nGHI\r\n"));
    963 	launder_static_string(term, CSI(1;2r));
    964 	s8 raw = launder_static_string(term, CSI(T));
    965 	handle_input(term, arena, raw);
    966 
    967 	Row *rows      = term->views[term->view_idx].fb.rows;
    968 	result.status  = rows[0][0].cp == ' ';
    969 	result.status &= rows[0][1].cp == ' ';
    970 	result.status &= rows[0][2].cp == ' ';
    971 	result.status &= rows[1][0].cp == 'A';
    972 	result.status &= rows[1][1].cp == 'B';
    973 	result.status &= rows[1][2].cp == 'C';
    974 	result.status &= rows[2][0].cp == 'G';
    975 	result.status &= rows[2][1].cp == 'H';
    976 	result.status &= rows[2][2].cp == 'I';
    977 
    978 	return result;
    979 }
    980 
    981 /* NOTE: DECSTBM V-4: Top Equal to Bottom */
    982 static TEST_FN(set_top_bottom_margins_v4)
    983 {
    984 	struct test_result result = {.info = __FUNCTION__};
    985 
    986 	launder_static_string(term, s8("ABC\r\nDEF\r\nGHI\r\n"));
    987 	launder_static_string(term, CSI(2;2r));
    988 	s8 raw = launder_static_string(term, CSI(T));
    989 	handle_input(term, arena, raw);
    990 
    991 	Row *rows      = term->views[term->view_idx].fb.rows;
    992 	result.status  = rows[0][0].cp == ' ';
    993 	result.status &= rows[0][1].cp == ' ';
    994 	result.status &= rows[0][2].cp == ' ';
    995 	result.status &= rows[1][0].cp == 'A';
    996 	result.status &= rows[1][1].cp == 'B';
    997 	result.status &= rows[1][2].cp == 'C';
    998 	result.status &= rows[2][0].cp == 'D';
    999 	result.status &= rows[2][1].cp == 'E';
   1000 	result.status &= rows[2][2].cp == 'F';
   1001 	result.status &= rows[3][0].cp == 'G';
   1002 	result.status &= rows[3][1].cp == 'H';
   1003 	result.status &= rows[3][2].cp == 'I';
   1004 
   1005 	return result;
   1006 }
   1007 
   1008 /* NOTE: DSR V-1: Operating Status */
   1009 static TEST_FN(device_status_report_v1)
   1010 {
   1011 	struct test_result result = {.info = __FUNCTION__};
   1012 
   1013 	Stream buffer = {.buf = malloc(KB(1)), .cap = KB(1)};
   1014 	term->child   = (iptr)&buffer;
   1015 	s8 raw = launder_static_string(term, CSI(5n));
   1016 	handle_input(term, arena, raw);
   1017 
   1018 	result.status = s8_equal(s8("\x1B[0n"), stream_to_s8(&buffer));
   1019 
   1020 	free(buffer.buf);
   1021 
   1022 	return result;
   1023 }
   1024 
   1025 /* NOTE: DSR V-2: Cursor Position */
   1026 static TEST_FN(device_status_report_v2)
   1027 {
   1028 	struct test_result result = {.info = __FUNCTION__};
   1029 
   1030 	Stream buffer = {.buf = malloc(KB(1)), .cap = KB(1)};
   1031 	term->child   = (iptr)&buffer;
   1032 
   1033 	launder_static_string(term, CSI(2;4H));
   1034 	s8 raw = launder_static_string(term, CSI(6n));
   1035 	handle_input(term, arena, raw);
   1036 
   1037 	result.status = s8_equal(s8("\x1B[2;4R"), stream_to_s8(&buffer));
   1038 
   1039 	free(buffer.buf);
   1040 
   1041 	return result;
   1042 }
   1043 
   1044 int
   1045 main(void)
   1046 {
   1047 	Arena log_backing = arena_from_memory_block(os_block_alloc(MB(16)));
   1048 	Stream log        = arena_stream(log_backing);
   1049 
   1050 	/* TODO: should probably be some odd size */
   1051 
   1052 	u32 max_name_len = 0;
   1053 	#define X(name) if (sizeof(#name) - 1 > max_name_len) max_name_len = sizeof(#name) - 1;
   1054 	TESTS
   1055 	GHOSTTY_TESTS
   1056 	#undef X
   1057 	max_name_len += 1;
   1058 
   1059 	u32 failure_count = 0;
   1060 	for (u32 i = 0; i < ARRAY_COUNT(tests); i++) {
   1061 		MemoryBlock term_backing = {.memory = malloc(MB(4)), .size = MB(4)};
   1062 		/* TODO(rnp): term sizes as part of the tests */
   1063 		Term *term = place_term_into_memory(term_backing, 24, 80);
   1064 		struct test_result result = tests[i](term, term->arena_for_frame);
   1065 		s8 fn = c_str_to_s8((char *)result.info);
   1066 		stream_push_s8(&log, fn);
   1067 		stream_push_s8(&log, s8(":"));
   1068 		size count = fn.len;
   1069 		while (count < max_name_len) { stream_push_byte(&log, ' '); count++; }
   1070 		switch (result.status) {
   1071 		case FAILURE:     stream_push_s8(&log, failure_string); failure_count++; break;
   1072 		case SUCCESS:     stream_push_s8(&log, success_string);                  break;
   1073 		case UNSUPPORTED: stream_push_s8(&log, unsupported_string);              break;
   1074 		}
   1075 		release_term_memory(term_backing);
   1076 	}
   1077 	stream_push_s8(&log, s8("FINISHED: ["));
   1078 	stream_push_u64(&log, ARRAY_COUNT(tests) - failure_count);
   1079 	stream_push_byte(&log, '/');
   1080 	stream_push_u64(&log, ARRAY_COUNT(tests));
   1081 	stream_push_s8(&log, s8("] Succeeded\n"));
   1082 	os_write_err_msg(stream_to_s8(&log));
   1083 	return 0;
   1084 }