// test_ansi_parser.cpp — tests unitarios para fn_term::AnsiParser. // // Logica pura: no requiere ImGui ni contexto GL. Cubre: // - SGR: reset, FG color, BG color, bright colors, bold // - Cursor moves: CUU/CUD/CUF/CUB, CUP // - ED(2) erase display, EL(2) erase line // - Texto normal + secuencias mixtas // - CR, LF, BS #define CATCH_CONFIG_MAIN #include "catch_amalgamated.hpp" #include "core/ansi_parser.h" #include #include using namespace fn_term; // Helper: parsea una cadena y colecta los eventos. static std::vector parse(const std::string& s) { AnsiParser p; std::vector evs; p.feed(s.c_str(), s.size(), [&](const AnsiEvent& ev) { evs.push_back(ev); }); return evs; } // Helper: obtiene estados SGR después de parsear (sin eventos de salida). struct SgrState { uint8_t fg; uint8_t bg; uint8_t bold; }; static SgrState parse_sgr(const std::string& s) { AnsiParser p; p.feed(s.c_str(), s.size(), [](const AnsiEvent&) {}); return {p.current_fg(), p.current_bg(), p.current_bold()}; } // --------------------------------------------------------------------------- // SGR tests // --------------------------------------------------------------------------- TEST_CASE("SGR reset sets default colors", "[ansi_parser][sgr]") { // Primero ponemos FG rojo, luego reset. auto st = parse_sgr("\x1b[31m\x1b[0m"); REQUIRE(st.fg == kColorDefault); REQUIRE(st.bg == kColorDefault); REQUIRE(st.bold == 0); } TEST_CASE("SGR fg color 31 sets red", "[ansi_parser][sgr]") { auto st = parse_sgr("\x1b[31m"); REQUIRE(st.fg == 1); // rojo = index 1 } TEST_CASE("SGR bg color 44 sets blue background", "[ansi_parser][sgr]") { auto st = parse_sgr("\x1b[44m"); REQUIRE(st.bg == 4); // azul = index 4 } TEST_CASE("SGR bright fg 91 sets bright red", "[ansi_parser][sgr]") { auto st = parse_sgr("\x1b[91m"); REQUIRE(st.fg == 9); // bright red = index 8+1 = 9 } TEST_CASE("SGR bold sets bold flag", "[ansi_parser][sgr]") { auto st = parse_sgr("\x1b[1m"); REQUIRE(st.bold == 1); } TEST_CASE("SGR reset via bare ESC[m", "[ansi_parser][sgr]") { // ESC [ m sin parametro = reset auto st = parse_sgr("\x1b[31m\x1b[m"); REQUIRE(st.fg == kColorDefault); } // --------------------------------------------------------------------------- // Cursor move tests // --------------------------------------------------------------------------- TEST_CASE("cursor CUU moves up N", "[ansi_parser][cursor]") { auto evs = parse("\x1b[3A"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::CursorMove); REQUIRE(evs[0].cursor_rel.dir == CursorDir::Up); REQUIRE(evs[0].cursor_rel.n == 3); } TEST_CASE("cursor CUF moves forward N", "[ansi_parser][cursor]") { auto evs = parse("\x1b[5C"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::CursorMove); REQUIRE(evs[0].cursor_rel.dir == CursorDir::Forward); REQUIRE(evs[0].cursor_rel.n == 5); } TEST_CASE("cursor CUB moves back 1 when no param", "[ansi_parser][cursor]") { auto evs = parse("\x1b[D"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::CursorMove); REQUIRE(evs[0].cursor_rel.dir == CursorDir::Back); REQUIRE(evs[0].cursor_rel.n == 1); } TEST_CASE("cursor CUP absolute position", "[ansi_parser][cursor]") { // ESC[5;10H → row=4, col=9 (0-based) auto evs = parse("\x1b[5;10H"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::CursorAbsolute); REQUIRE(evs[0].cursor_abs.row == 4); REQUIRE(evs[0].cursor_abs.col == 9); } TEST_CASE("cursor CUP default params (ESC[H) = origin", "[ansi_parser][cursor]") { auto evs = parse("\x1b[H"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::CursorAbsolute); REQUIRE(evs[0].cursor_abs.row == 0); REQUIRE(evs[0].cursor_abs.col == 0); } // --------------------------------------------------------------------------- // Erase tests // --------------------------------------------------------------------------- TEST_CASE("erase display ED 2", "[ansi_parser][erase]") { auto evs = parse("\x1b[2J"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::EraseDisplay); } TEST_CASE("erase line EL 2", "[ansi_parser][erase]") { auto evs = parse("\x1b[2K"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::EraseLine); } // --------------------------------------------------------------------------- // Control chars // --------------------------------------------------------------------------- TEST_CASE("newline and carriage return", "[ansi_parser][control]") { auto evs = parse("\r\n"); REQUIRE(evs.size() == 2); REQUIRE(evs[0].type == AnsiEventType::CarriageReturn); REQUIRE(evs[1].type == AnsiEventType::Newline); } TEST_CASE("backspace emits Backspace event", "[ansi_parser][control]") { auto evs = parse("\x08"); REQUIRE(evs.size() == 1); REQUIRE(evs[0].type == AnsiEventType::Backspace); } // --------------------------------------------------------------------------- // Text + mixed sequences // --------------------------------------------------------------------------- TEST_CASE("plain text emits Char events", "[ansi_parser][text]") { auto evs = parse("hi"); REQUIRE(evs.size() == 2); REQUIRE(evs[0].type == AnsiEventType::Char); REQUIRE(evs[0].cell.ch == U'h'); REQUIRE(evs[1].cell.ch == U'i'); } TEST_CASE("mixed text and SGR sequence", "[ansi_parser][mixed]") { // "A" con FG rojo, luego reset, luego "B". auto evs = parse("\x1b[31mA\x1b[0mB"); // Debemos tener exactamente 2 eventos Char: A (fg=1) y B (fg=default). REQUIRE(evs.size() == 2); REQUIRE(evs[0].type == AnsiEventType::Char); REQUIRE(evs[0].cell.ch == U'A'); REQUIRE(evs[0].cell.fg == 1); // rojo REQUIRE(evs[1].type == AnsiEventType::Char); REQUIRE(evs[1].cell.ch == U'B'); REQUIRE(evs[1].cell.fg == kColorDefault); } TEST_CASE("char inherits current SGR attrs", "[ansi_parser][sgr]") { AnsiParser p; std::vector evs; // Poner BG azul, luego emitir texto. std::string s = "\x1b[44mX"; p.feed(s.c_str(), s.size(), [&](const AnsiEvent& ev) { evs.push_back(ev); }); REQUIRE(evs.size() == 1); REQUIRE(evs[0].cell.ch == U'X'); REQUIRE(evs[0].cell.bg == 4); // azul } TEST_CASE("unknown CSI final byte ignored silently", "[ansi_parser][robustness]") { // ESC [ Z es desconocido — no debe emitir nada ni crashear. auto evs = parse("a\x1b[Zb"); REQUIRE(evs.size() == 2); REQUIRE(evs[0].cell.ch == U'a'); REQUIRE(evs[1].cell.ch == U'b'); } TEST_CASE("incomplete escape at end of buffer", "[ansi_parser][robustness]") { // Buffer termina a mitad de una secuencia — no debe crashear. AnsiParser p; std::string s1 = "\x1b[3"; std::string s2 = "1m"; p.feed(s1.c_str(), s1.size(), [](const AnsiEvent&) {}); p.feed(s2.c_str(), s2.size(), [](const AnsiEvent&) {}); REQUIRE(p.current_fg() == 1); // FG rojo aplicado correctamente } TEST_CASE("reset() clears state", "[ansi_parser][reset]") { AnsiParser p; std::string s = "\x1b[31m"; // FG rojo p.feed(s.c_str(), s.size(), [](const AnsiEvent&) {}); REQUIRE(p.current_fg() == 1); p.reset(); REQUIRE(p.current_fg() == kColorDefault); }