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 }