vtgl

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

test.c (31487B)


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