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 }