test.c (11285B)
1 /* See LICENSE for copyright details */ 2 #include "util.h" 3 #include "config.h" 4 5 /* NOTE: stubs for stuff we aren't testing */ 6 static void get_gpu_glyph_index(Arena, void *, void *, u32, u32, u32, CachedGlyph **); 7 8 KEYBIND_FN(copy) { return 0; } 9 KEYBIND_FN(paste) { return 0; } 10 KEYBIND_FN(scroll) { return 0; } 11 KEYBIND_FN(zoom) { return 0; } 12 13 #include "terminal.c" 14 15 static void 16 get_gpu_glyph_index(Arena a, void *b, void *c, u32 cp, u32 d, u32 e, CachedGlyph **cg) 17 { 18 /* TODO: this is wrong but will have to do for these tests */ 19 static CachedGlyph scg; 20 if (BETWEEN(cp, ' ', '~')) scg.tile_count = 1; 21 else scg.tile_count = 2; 22 *cg = &scg; 23 } 24 25 static b32 26 mem_cmp(void *a_, void *b_, size len) 27 { 28 /* NOTE: small size assumption */ 29 ASSERT(len < 32); 30 b32 result = 0; 31 u8 *a = a_, *b = b_; 32 for (size i = 0; i < len; i++) 33 result |= a[i] != b[i]; 34 return result; 35 } 36 37 struct test_result { b32 status; const char *info; }; 38 #define TEST_FN(name) struct test_result name(Term *term, Arena arena) 39 typedef TEST_FN(Test_Fn); 40 41 #define TESTS \ 42 X(csi_embedded_control) \ 43 X(colour_setting) \ 44 X(cursor_at_line_boundary) \ 45 X(cursor_movement) \ 46 X(cursor_tabs) \ 47 X(cursor_tabs_across_boundary) \ 48 X(working_ringbuffer) 49 50 #define X(fn) static TEST_FN(fn); 51 TESTS 52 #undef X 53 54 #define X(fn) fn, 55 static Test_Fn *tests[] = { 56 TESTS 57 }; 58 #undef X 59 60 #define ESC(a) s8("\x1B"#a) 61 #define CSI(a) ESC([a) 62 63 static s8 failure_string = s8("\x1B[31mFAILURE\x1B[0m\n"); 64 static s8 success_string = s8("\x1B[32mSUCCESS\x1B[0m\n"); 65 66 static void 67 buffer_reset(Term *t) 68 { 69 t->views[0].log.widx = 0; 70 t->views[0].log.filled = 0; 71 t->views[1].log.widx = 0; 72 t->views[1].log.filled = 0; 73 t->unprocessed_bytes = 0; 74 t->views[0].lines.widx = 0; 75 t->views[0].lines.filled = 0; 76 t->views[1].lines.widx = 0; 77 t->views[1].lines.filled = 0; 78 } 79 80 static size 81 copy_into_ringbuf(RingBuf *rb, s8 raw) 82 { 83 ASSERT(raw.len < rb->cap); 84 for (size i = 0; i < raw.len; i++) 85 rb->buf[rb->widx + i] = raw.data[i]; 86 87 rb->widx += raw.len; 88 rb->filled += raw.len; 89 90 CLAMP(rb->filled, 0, rb->cap); 91 if (rb->widx >= rb->cap) 92 rb->widx -= rb->cap; 93 94 ASSERT(rb->filled >= 0); 95 ASSERT(rb->widx >= 0 && rb->widx < rb->cap); 96 return raw.len; 97 } 98 99 static b32 100 check_cells_equal(Cell *a, Cell *b) 101 { 102 b32 result = a->cp == b->cp && 103 a->bg == b->bg && 104 a->fg == b->fg; 105 return result; 106 } 107 108 static s8 109 launder_static_string(Term *term, s8 static_str) 110 { 111 RingBuf *rb = &term->views[term->view_idx].log; 112 term->unprocessed_bytes += copy_into_ringbuf(rb, static_str); 113 s8 raw = { 114 .len = term->unprocessed_bytes, 115 .data = rb->buf + (rb->widx - term->unprocessed_bytes) 116 }; 117 return raw; 118 } 119 120 static TEST_FN(csi_embedded_control) 121 { 122 struct test_result result = {.info = __FUNCTION__}; 123 124 /* NOTE: print a '1' with default style then start changing the colour, 125 * but backspace within the escape sequence so the cursor is now over the 126 * '1', but then put a newline inside the sequence so that cursor is on 127 * the next line, finally print a ' 2' with the completed colour sequence. */ 128 launder_static_string(term, s8("1")); 129 launder_static_string(term, CSI(48;2;7\b5;63;4\n2m)); 130 s8 raw = launder_static_string(term, s8(" 2")); 131 handle_input(term, arena, raw); 132 133 #if 0 134 dump_csi(&term->csi); 135 #endif 136 137 CellStyle final_style = { 138 .bg = (Colour){.r = 75, .g = 63, .b = 42, .a = 0xFF}, 139 .fg = g_colours.data[g_colours.fgidx], 140 .attr = ATTR_NULL, 141 }; 142 143 Cell c1 = {.cp = '1', 144 .bg = SHADER_PACK_BG(g_colours.data[g_colours.bgidx].rgba, ATTR_NULL), 145 .fg = SHADER_PACK_FG(g_colours.data[g_colours.fgidx].rgba, ATTR_NULL), 146 }; 147 Cell c2 = {.cp = '2', 148 .bg = SHADER_PACK_BG(final_style.bg.rgba, final_style.attr), 149 .fg = SHADER_PACK_FG(final_style.fg.rgba, final_style.attr), 150 }; 151 152 result.status = term->cursor.pos.y == 1 && term->cursor.pos.x == 2; 153 result.status &= check_cells_equal(&c1, &term->views[term->view_idx].fb.rows[0][0]); 154 result.status &= check_cells_equal(&c2, &term->views[term->view_idx].fb.rows[1][1]); 155 /* NOTE: we also want to ensure that we cannot split a line in the middle of a CSI */ 156 LineBuf *lb = &term->views[0].lines; 157 result.status &= lb->filled == 0 && *lb->buf[lb->widx].start != '2'; 158 159 return result; 160 } 161 162 static TEST_FN(colour_setting) 163 { 164 struct test_result result = {.info = __FUNCTION__}; 165 166 launder_static_string(term, CSI(8m)); 167 launder_static_string(term, CSI(4m)); 168 launder_static_string(term, CSI(9m)); 169 launder_static_string(term, CSI(24m)); 170 launder_static_string(term, CSI(33m)); 171 launder_static_string(term, CSI(48;2;75;63;42m)); 172 s8 raw = launder_static_string(term, s8("A")); 173 174 handle_input(term, arena, raw); 175 176 u32 attr = (ATTR_INVISIBLE|ATTR_STRUCK); 177 Cell c = {.cp = 'A', 178 .bg = SHADER_PACK_BG(((Colour){.r = 75, .g = 63, .b = 42, .a = 0xFF}.rgba), attr), 179 .fg = SHADER_PACK_FG(g_colours.data[3].rgba, attr), 180 }; 181 result.status = check_cells_equal(&c, term->views[term->view_idx].fb.rows[0]); 182 183 return result; 184 } 185 186 static TEST_FN(cursor_at_line_boundary) 187 { 188 /* NOTE: Test that lines are not split in the middle of utf-8 characters */ 189 struct test_result result = {.info = __FUNCTION__}; 190 191 s8 long_line = s8alloc(&arena, 8192); 192 mem_clear(long_line.data, ' ', long_line.len); 193 194 /* NOTE: change the cursor state in the middle of the line */ 195 long_line.data[128 + 0] = 0x1B; 196 long_line.data[128 + 1] = '['; 197 long_line.data[128 + 2] = '5'; 198 long_line.data[128 + 3] = 'm'; 199 200 /* NOTE: place a non-ASCII character on the long line edge */ 201 s8 red_dragon = utf8_encode(0x1F004); 202 long_line.data[SPLIT_LONG - 1] = red_dragon.data[0]; 203 long_line.data[SPLIT_LONG + 0] = red_dragon.data[1]; 204 205 s8 raw = launder_static_string(term, (s8){.len = SPLIT_LONG + 1, .data = long_line.data}); 206 long_line = consume(long_line, SPLIT_LONG + 1); 207 208 LineBuf *lb = &term->views[term->view_idx].lines; 209 size line_count = lb->filled; 210 handle_input(term, arena, raw); 211 212 /* NOTE: ensure line didn't split on red dragon */ 213 result.status = term->unprocessed_bytes != 0; 214 215 long_line.data[0] = red_dragon.data[2]; 216 long_line.data[1] = red_dragon.data[3]; 217 218 /* NOTE: shove a newline at the end so that the line completes */ 219 long_line.data[long_line.len - 1] = '\n'; 220 221 #if 0 222 long_line.data[SPLIT_LONG + 3] = 0; 223 printf("encoded char: %s\n", (char *)(long_line.data + SPLIT_LONG - 1)); 224 long_line.data[SPLIT_LONG + 3] = ' '; 225 #endif 226 227 raw = launder_static_string(term, long_line); 228 handle_input(term, arena, raw); 229 230 result.status &= line_length(lb->buf) > SPLIT_LONG; 231 232 return result; 233 } 234 235 236 static TEST_FN(cursor_movement) 237 { 238 struct test_result result = {.info = __FUNCTION__}; 239 240 s8 raw = launder_static_string(term, CSI(17;2H)); 241 handle_input(term, arena, raw); 242 243 raw = launder_static_string(term, CSI(7G)); 244 handle_input(term, arena, raw); 245 246 result.status = term->cursor.pos.y == 16 && term->cursor.pos.x == 6; 247 248 return result; 249 } 250 251 static TEST_FN(cursor_tabs) 252 { 253 struct test_result result = {.info = __FUNCTION__}; 254 255 /* NOTE: first test advancing to a tabstop */ 256 s8 raw = launder_static_string(term, s8("123\t")); 257 handle_input(term, arena, raw); 258 259 result.status = term->cursor.pos.x == (g_tabstop); 260 261 /* NOTE: now test negative tabstop movement and tabstop setting */ 262 launder_static_string(term, s8("12")); 263 launder_static_string(term, ESC(H)); 264 launder_static_string(term, s8("34\t")); 265 raw = launder_static_string(term, CSI(2Z)); 266 handle_input(term, arena, raw); 267 268 result.status &= term->cursor.pos.x == (g_tabstop); 269 270 return result; 271 } 272 273 static TEST_FN(cursor_tabs_across_boundary) 274 { 275 struct test_result result = {.info = __FUNCTION__}; 276 277 /* NOTE: clear tabstops then set one beyond multiple boundaries */ 278 launder_static_string(term, CSI(3g)); 279 launder_static_string(term, CSI(1;67H)); 280 launder_static_string(term, ESC(H)); 281 launder_static_string(term, CSI(1;1H)); 282 s8 raw = launder_static_string(term, s8("\t")); 283 handle_input(term, arena, raw); 284 285 result.status = term->cursor.pos.x == 66; 286 287 /* NOTE: now set one exactly on a boundary */ 288 launder_static_string(term, CSI(1;34H)); 289 launder_static_string(term, ESC(H)); 290 launder_static_string(term, CSI(1;1H)); 291 raw = launder_static_string(term, s8("\t")); 292 handle_input(term, arena, raw); 293 294 result.status &= term->cursor.pos.x == 33; 295 296 /* NOTE: now set one right before the previous */ 297 launder_static_string(term, CSI(1;33H)); 298 launder_static_string(term, ESC(H)); 299 launder_static_string(term, CSI(1;1H)); 300 301 raw = launder_static_string(term, s8("\t")); 302 handle_input(term, arena, raw); 303 result.status &= term->cursor.pos.x == 32; 304 305 raw = launder_static_string(term, s8("\t")); 306 handle_input(term, arena, raw); 307 result.status &= term->cursor.pos.x == 33; 308 309 /* NOTE: ensure backwards tab works */ 310 launder_static_string(term, CSI(1;34H)); 311 launder_static_string(term, ESC(0g)); 312 launder_static_string(term, CSI(1;66H)); 313 raw = launder_static_string(term, CSI(1Z)); 314 handle_input(term, arena, raw); 315 result.status &= term->cursor.pos.x == 33; 316 317 return result; 318 } 319 320 static TEST_FN(working_ringbuffer) 321 { 322 struct test_result result = {.info = __FUNCTION__}; 323 RingBuf *rb = &term->views[term->view_idx].log; 324 rb->buf[0] = 0xFE; 325 result.status = (rb->buf[0] == rb->buf[ rb->cap]) && 326 (rb->buf[0] == rb->buf[-rb->cap]); 327 return result; 328 } 329 330 int 331 main(void) 332 { 333 Arena memory = arena_from_memory_block(posix_alloc(32 * MEGABYTE)); 334 Term term = {0}; 335 u32 failure_count = 0; 336 337 Stream log = stream_alloc(&memory, 4 * MEGABYTE); 338 for (u32 i = 0; i < ARRAY_COUNT(term.saved_cursors); i++) { 339 cursor_reset(&term); 340 cursor_move_to(&term, 0, 0); 341 cursor_alt(&term, 1); 342 } 343 344 posix_allocate_ring_buffer(&term.views[0].log, BACKLOG_SIZE); 345 line_buf_alloc(&term.views[0].lines, &memory, term.views[0].log.buf, term.cursor.style, 346 BACKLOG_LINES); 347 posix_allocate_ring_buffer(&term.views[1].log, ALT_BACKLOG_SIZE); 348 line_buf_alloc(&term.views[1].lines, &memory, term.views[1].log.buf, term.cursor.style, 349 ALT_BACKLOG_LINES); 350 351 /* TODO: should probably be some odd size */ 352 term.size = (uv2){.w = 80, .h = 24}; 353 term.views[0].fb.backing_store = memory_block_from_arena(&memory, 2 * MEGABYTE); 354 term.views[1].fb.backing_store = memory_block_from_arena(&memory, 2 * MEGABYTE); 355 initialize_framebuffer(&term.views[0].fb, term.size); 356 initialize_framebuffer(&term.views[1].fb, term.size); 357 358 u32 max_name_len = 0; 359 #define X(name) if (sizeof(#name) - 1 > max_name_len) max_name_len = sizeof(#name) - 1; 360 TESTS 361 #undef X 362 max_name_len += 1; 363 364 for (u32 i = 0; i < ARRAY_COUNT(tests); i++) { 365 buffer_reset(&term); 366 term_reset(&term); 367 struct test_result result = tests[i](&term, memory); 368 s8 fn = c_str_to_s8((char *)result.info); 369 stream_push_s8(&log, fn); 370 stream_push_s8(&log, s8(":")); 371 size count = fn.len; 372 while (count < max_name_len) { stream_push_byte(&log, ' '); count++; } 373 if (result.status == 0) { 374 failure_count++; 375 stream_push_s8(&log, failure_string); 376 } else { 377 stream_push_s8(&log, success_string); 378 } 379 } 380 stream_push_s8(&log, s8("FINISHED: [")); 381 stream_push_u64(&log, ARRAY_COUNT(tests) - failure_count); 382 stream_push_byte(&log, '/'); 383 stream_push_u64(&log, ARRAY_COUNT(tests)); 384 stream_push_s8(&log, s8("] Succeeded\n")); 385 os_write_err_msg(stream_to_s8(&log)); 386 return 0; 387 }