07252c0172
Nuevo modulo reutilizable terminal_panel (fn_term) para ImGui: Sub-fn ansi_parser_cpp_core (cpp/functions/core/): - Parser ANSI/VT100 byte-a-byte sin heap allocs por evento - SGR colores FG/BG 16-color + bold + reset - Cursor moves CUU/CUD/CUF/CUB + CUP absoluto - Erase ED(2)/EL(2), CR/LF/BS - Statemachine 4 estados, thread-unsafe por diseno - 21 tests unitarios (57 assertions), todos pasan terminal_panel_cpp_viz (cpp/functions/viz/terminal_panel/): - terminal_panel.cpp: render ImGui + process_output con list clipper - terminal_panel_linux.cpp: forkpty + reader thread no-blocking - terminal_panel_windows.cpp: ConPTY CreatePseudoConsole (SDK >= 17763) - Scrollback circular configurable (default 5000 lineas) - Toolbar: clear, copy, reset, scroll-lock + status indicator - readonly mode: sin input box, send() es no-op - uses_functions: ansi_parser_cpp_core, logger_cpp_core Tests: - test_ansi_parser.cpp: 21 test cases, 57 assertions (PASS) - test_terminal_panel_smoke.cpp: 3 test cases (PASS: spawn echo hello, process exits cleanly, readonly ignores send) CMake: - cpp/tests/CMakeLists.txt: add test_ansi_parser + test_terminal_panel_smoke - primitives_gallery (sub-repo): ver commit separado en apps/primitives_gallery Pendiente (anti-scope v1): - Windows ConPTY: stub funcional que compila; join() del reader thread via std::thread no implementado (usa CreateThread detached) - ANSI 256/24-bit color, italics, Unicode wide - Curses pesados (vim, htop, top) — cursor visible basic solo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
3.4 KiB
C++
111 lines
3.4 KiB
C++
// test_terminal_panel_smoke.cpp — smoke test para terminal_panel.
|
|
//
|
|
// Prueba real del PTY en Linux: spawn "echo hello && exit 0",
|
|
// espera output, verifica que el scrollback contiene "hello".
|
|
//
|
|
// En Windows: test skipped (ConPTY require DISPLAY y proceso vivo — CI).
|
|
// En Linux sin forkpty: verifica que el build es correcto al menos.
|
|
|
|
#define CATCH_CONFIG_MAIN
|
|
#include "catch_amalgamated.hpp"
|
|
|
|
#include "viz/terminal_panel/terminal_panel.h"
|
|
|
|
#include <chrono>
|
|
#include <string>
|
|
#include <thread>
|
|
|
|
#ifdef _WIN32
|
|
// En Windows en CI, skipeamos el smoke del proceso real.
|
|
TEST_CASE("smoke: spawn echo hello and exit, scrollback contains hello", "[terminal_panel][smoke]") {
|
|
SKIP("Smoke PTY test skipped on Windows CI");
|
|
}
|
|
#else
|
|
|
|
// Helper: concatena todas las celdas del scrollback como texto plano.
|
|
static std::string scrollback_text(fn_term::TerminalPanel& p) {
|
|
std::lock_guard<std::mutex> lk(p.buf_mutex);
|
|
std::string result;
|
|
for (const auto& line : p.lines) {
|
|
for (const auto& cell : line) {
|
|
if (cell.ch >= 0x20 && cell.ch < 0x7F)
|
|
result += static_cast<char>(cell.ch);
|
|
}
|
|
result += '\n';
|
|
}
|
|
return result;
|
|
}
|
|
|
|
TEST_CASE("smoke: spawn echo hello and exit, scrollback contains hello", "[terminal_panel][smoke]") {
|
|
fn_term::TerminalPanel term;
|
|
term.shell = "/bin/bash";
|
|
term.scrollback_lines = 100;
|
|
|
|
fn_term::open(term);
|
|
REQUIRE(term.is_open());
|
|
|
|
// Enviar el comando y esperar a que el proceso salga.
|
|
fn_term::send(term, "echo hello && exit 0\n");
|
|
|
|
// Esperar máximo 2 segundos a que el proceso termine.
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
while (!term.process_exited.load()
|
|
&& std::chrono::steady_clock::now() < deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
|
|
// Dar 100ms adicionales para que el reader thread procese el último output.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
|
|
std::string text = scrollback_text(term);
|
|
fn_term::close(term);
|
|
|
|
INFO("scrollback: " << text);
|
|
REQUIRE(text.find("hello") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("smoke: process exits cleanly", "[terminal_panel][smoke]") {
|
|
fn_term::TerminalPanel term;
|
|
term.shell = "/bin/bash";
|
|
term.scrollback_lines = 50;
|
|
|
|
fn_term::open(term);
|
|
REQUIRE(term.is_open());
|
|
|
|
fn_term::send(term, "exit 0\n");
|
|
|
|
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
while (!term.process_exited.load()
|
|
&& std::chrono::steady_clock::now() < deadline) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
|
}
|
|
|
|
REQUIRE(term.process_exited.load());
|
|
REQUIRE(term.exit_code == 0);
|
|
|
|
fn_term::close(term);
|
|
}
|
|
|
|
TEST_CASE("smoke: readonly panel ignores send", "[terminal_panel][smoke]") {
|
|
fn_term::TerminalPanel term;
|
|
term.shell = "/bin/bash";
|
|
term.readonly = true;
|
|
term.scrollback_lines = 50;
|
|
|
|
fn_term::open(term);
|
|
REQUIRE(term.is_open());
|
|
|
|
// send() no debe hacer nada (readonly).
|
|
fn_term::send(term, "echo should_not_appear\n");
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
|
|
std::string text = scrollback_text(term);
|
|
fn_term::close(term);
|
|
|
|
// "should_not_appear" no debería estar en el scrollback porque send es no-op.
|
|
INFO("scrollback: " << text);
|
|
REQUIRE(text.find("should_not_appear") == std::string::npos);
|
|
}
|
|
|
|
#endif // !_WIN32
|