From 4f280f34d7a3043a6ede8a282e77b99d8d577a72 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Tue, 28 Apr 2026 23:58:40 +0200 Subject: [PATCH] test(cpp): tests para sql_parse, process_state_machine, file_poll_diff --- cpp/functions/core/sql_parse.cpp | 19 ++-- cpp/tests/CMakeLists.txt | 8 ++ cpp/tests/test_file_poll_diff.cpp | 105 +++++++++++++++++++++++ cpp/tests/test_process_state_machine.cpp | 83 ++++++++++++++++++ cpp/tests/test_sql_parse.cpp | 93 ++++++++++++++++++++ 5 files changed, 302 insertions(+), 6 deletions(-) create mode 100644 cpp/tests/test_file_poll_diff.cpp create mode 100644 cpp/tests/test_process_state_machine.cpp create mode 100644 cpp/tests/test_sql_parse.cpp diff --git a/cpp/functions/core/sql_parse.cpp b/cpp/functions/core/sql_parse.cpp index 22b9e3e2..a06e9a06 100644 --- a/cpp/functions/core/sql_parse.cpp +++ b/cpp/functions/core/sql_parse.cpp @@ -101,18 +101,25 @@ std::vector sql_parse(const std::string& input) { switch (m) { case Mode::Normal: - if (!stmt_has_content && !std::isspace(static_cast(c))) { - // marca inicio "real" de un statement (despues de skip ws/comments) - stmt_line = cur_line; - stmt_has_content = true; - } if (c == '-' && n == '-') { m = Mode::LineComment; ++i; - if (n == '\n') ++cur_line; // (no aplica, n es '-') } else if (c == '/' && n == '*') { m = Mode::BlockComment; ++i; + } else if (!stmt_has_content && !std::isspace(static_cast(c))) { + // marca inicio "real" de un statement (despues de skip ws/comments) + stmt_line = cur_line; + stmt_has_content = true; + // procesar el caracter actual abajo (no es comentario ni ws) + if (c == '\'') m = Mode::SingleStr; + else if (c == '"') m = Mode::DoubleStr; + else if (c == '`') m = Mode::BackTick; + else if (c == ';') { + flush(i); + stmt_start = i + 1; + stmt_has_content = false; + } } else if (c == '\'') { m = Mode::SingleStr; } else if (c == '"') { diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 64711ac0..2040e966 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -31,6 +31,14 @@ add_fn_test(test_pie_chart_math test_pie_chart_math.cpp) add_fn_test(test_kpi_card_math test_kpi_card_math.cpp) add_fn_test(test_bar_chart_math test_bar_chart_math.cpp) +# Issue 0045 — tests de la logica pura extraida. +add_fn_test(test_sql_parse test_sql_parse.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/sql_parse.cpp) +add_fn_test(test_process_state_machine test_process_state_machine.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/process_state_machine.cpp) +add_fn_test(test_file_poll_diff test_file_poll_diff.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/file_poll_diff.cpp) + # --- Placeholders para primitivos UI (logica visual cubierta en 0048) ------ add_fn_test(test_tokens test_tokens.cpp) add_fn_test(test_button test_button.cpp) diff --git a/cpp/tests/test_file_poll_diff.cpp b/cpp/tests/test_file_poll_diff.cpp new file mode 100644 index 00000000..b4734cbc --- /dev/null +++ b/cpp/tests/test_file_poll_diff.cpp @@ -0,0 +1,105 @@ +// Tests for fn_ui::file_poll_diff (cpp/functions/core/file_poll_diff). +// Pura: compara dos snapshots de FS y devuelve added/modified/removed. + +#define CATCH_CONFIG_MAIN +#include "catch_amalgamated.hpp" + +#include "core/file_poll_diff.h" + +#include +#include +#include + +using fn_ui::file_poll_diff; +using fn_ui::FileEntry; + +namespace { +// Helper: ordena los vectores para comparaciones estables (el orden interno +// depende del orden de iteracion de unordered_map, no es estable). +void sort_diff(fn_ui::FileDiff& d) { + std::sort(d.added.begin(), d.added.end()); + std::sort(d.modified.begin(), d.modified.end()); + std::sort(d.removed.begin(), d.removed.end()); +} +} + +TEST_CASE("file_poll_diff detects added/modified/removed") { + std::vector before = {{"a", 10, 1}, {"b", 20, 2}, {"c", 30, 3}}; + std::vector after = {{"a", 10, 1}, {"b", 25, 2}, {"d", 40, 4}}; + auto d = file_poll_diff(before, after); + sort_diff(d); + REQUIRE(d.added == std::vector{"d"}); + REQUIRE(d.modified == std::vector{"b"}); + REQUIRE(d.removed == std::vector{"c"}); +} + +TEST_CASE("file_poll_diff: empty inputs") { + auto d = file_poll_diff({}, {}); + REQUIRE(d.added.empty()); + REQUIRE(d.modified.empty()); + REQUIRE(d.removed.empty()); +} + +TEST_CASE("file_poll_diff: identical snapshots") { + std::vector snap = {{"a", 1, 100}, {"b", 2, 200}}; + auto d = file_poll_diff(snap, snap); + REQUIRE(d.added.empty()); + REQUIRE(d.modified.empty()); + REQUIRE(d.removed.empty()); +} + +TEST_CASE("file_poll_diff: all added (before vacio)") { + std::vector after = {{"a", 1, 1}, {"b", 2, 2}}; + auto d = file_poll_diff({}, after); + sort_diff(d); + REQUIRE(d.added == std::vector{"a", "b"}); + REQUIRE(d.modified.empty()); + REQUIRE(d.removed.empty()); +} + +TEST_CASE("file_poll_diff: all removed (after vacio)") { + std::vector before = {{"a", 1, 1}, {"b", 2, 2}}; + auto d = file_poll_diff(before, {}); + sort_diff(d); + REQUIRE(d.removed == std::vector{"a", "b"}); + REQUIRE(d.modified.empty()); + REQUIRE(d.added.empty()); +} + +TEST_CASE("file_poll_diff: solo size cambia -> modified") { + std::vector before = {{"a", 100, 5}}; + std::vector after = {{"a", 200, 5}}; + auto d = file_poll_diff(before, after); + REQUIRE(d.modified == std::vector{"a"}); + REQUIRE(d.added.empty()); + REQUIRE(d.removed.empty()); +} + +TEST_CASE("file_poll_diff: solo mtime cambia -> modified") { + std::vector before = {{"a", 100, 5}}; + std::vector after = {{"a", 100, 9}}; + auto d = file_poll_diff(before, after); + REQUIRE(d.modified == std::vector{"a"}); + REQUIRE(d.added.empty()); + REQUIRE(d.removed.empty()); +} + +TEST_CASE("file_poll_diff: combinacion compleja") { + std::vector before = { + {"keep", 10, 1}, + {"mod_size", 20, 2}, + {"mod_mtime", 30, 3}, + {"removed", 40, 4}, + }; + std::vector after = { + {"keep", 10, 1}, + {"mod_size", 21, 2}, + {"mod_mtime", 30, 9}, + {"new", 50, 5}, + }; + auto d = file_poll_diff(before, after); + sort_diff(d); + REQUIRE(d.added == std::vector{"new"}); + REQUIRE(d.modified == std::vector{"mod_mtime", "mod_size"}); + REQUIRE(d.removed == std::vector{"removed"}); +} diff --git a/cpp/tests/test_process_state_machine.cpp b/cpp/tests/test_process_state_machine.cpp new file mode 100644 index 00000000..1f51a303 --- /dev/null +++ b/cpp/tests/test_process_state_machine.cpp @@ -0,0 +1,83 @@ +// Tests for fn_ui::process_transition (cpp/functions/core/process_state_machine). +// Tabla de transiciones definida en .md. + +#define CATCH_CONFIG_MAIN +#include "catch_amalgamated.hpp" + +#include "core/process_state_machine.h" +#include + +using fn_ui::process_transition; +using fn_ui::process_state_name; +using fn_ui::process_event_name; +using fn_ui::ProcessState; +using fn_ui::ProcessEvent; + +TEST_CASE("process_transition: idle -> running on Spawned") { + REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Spawned) == ProcessState::Running); +} + +TEST_CASE("process_transition: idle + Trigger sigue Idle (la solicitud no muta el estado)") { + REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Trigger) == ProcessState::Idle); +} + +TEST_CASE("process_transition: running -> success on Finished") { + REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Finished) == ProcessState::Success); +} + +TEST_CASE("process_transition: running -> error on Failed/Timeout") { + REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Failed) == ProcessState::Error); + REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Timeout) == ProcessState::Error); +} + +TEST_CASE("process_transition: success/error -> idle on Reset") { + REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Reset) == ProcessState::Idle); + REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Reset) == ProcessState::Idle); +} + +TEST_CASE("process_transition: invalid events keep state") { + // Idle no acepta Finished/Failed/Timeout. + REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Finished) == ProcessState::Idle); + REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Failed) == ProcessState::Idle); + REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Timeout) == ProcessState::Idle); + REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Reset) == ProcessState::Idle); + + // Running ignora Trigger/Spawned/Reset. + REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Trigger) == ProcessState::Running); + REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Spawned) == ProcessState::Running); + REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Reset) == ProcessState::Running); + + // Success ignora todo menos Reset. + REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Trigger) == ProcessState::Success); + REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Spawned) == ProcessState::Success); + REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Finished) == ProcessState::Success); + REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Failed) == ProcessState::Success); + + // Error idem. + REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Finished) == ProcessState::Error); + REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Spawned) == ProcessState::Error); +} + +TEST_CASE("process_state_name and process_event_name") { + REQUIRE(std::strcmp(process_state_name(ProcessState::Idle), "Idle") == 0); + REQUIRE(std::strcmp(process_state_name(ProcessState::Running), "Running") == 0); + REQUIRE(std::strcmp(process_state_name(ProcessState::Success), "Success") == 0); + REQUIRE(std::strcmp(process_state_name(ProcessState::Error), "Error") == 0); + + REQUIRE(std::strcmp(process_event_name(ProcessEvent::Trigger), "Trigger") == 0); + REQUIRE(std::strcmp(process_event_name(ProcessEvent::Spawned), "Spawned") == 0); + REQUIRE(std::strcmp(process_event_name(ProcessEvent::Finished), "Finished") == 0); + REQUIRE(std::strcmp(process_event_name(ProcessEvent::Failed), "Failed") == 0); + REQUIRE(std::strcmp(process_event_name(ProcessEvent::Timeout), "Timeout") == 0); + REQUIRE(std::strcmp(process_event_name(ProcessEvent::Reset), "Reset") == 0); +} + +TEST_CASE("process_transition: ciclo completo idle -> running -> success -> idle") { + ProcessState s = ProcessState::Idle; + s = process_transition(s, ProcessEvent::Spawned); + REQUIRE(s == ProcessState::Running); + s = process_transition(s, ProcessEvent::Finished); + REQUIRE(s == ProcessState::Success); + s = process_transition(s, ProcessEvent::Reset); + REQUIRE(s == ProcessState::Idle); +} diff --git a/cpp/tests/test_sql_parse.cpp b/cpp/tests/test_sql_parse.cpp new file mode 100644 index 00000000..40aa5bcc --- /dev/null +++ b/cpp/tests/test_sql_parse.cpp @@ -0,0 +1,93 @@ +// Tests for fn_ui::sql_parse / sql_classify (cpp/functions/core/sql_parse). +// Pura: tokeniza SQL multi-statement saltando strings y comentarios, y +// clasifica por keyword inicial. + +#define CATCH_CONFIG_MAIN +#include "catch_amalgamated.hpp" + +#include "core/sql_parse.h" + +using fn_ui::sql_parse; +using fn_ui::sql_classify; +using fn_ui::SqlStmtKind; + +TEST_CASE("sql_parse classifies common statements") { + auto stmts = sql_parse("SELECT * FROM t; INSERT INTO t VALUES (1);"); + REQUIRE(stmts.size() == 2); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); + REQUIRE(stmts[1].kind == SqlStmtKind::Insert); +} + +TEST_CASE("sql_parse handles strings and comments") { + auto stmts = sql_parse("-- comment\nSELECT 'a;b' FROM t; /* x */ DELETE FROM t;"); + REQUIRE(stmts.size() == 2); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); + REQUIRE(stmts[1].kind == SqlStmtKind::Delete); +} + +TEST_CASE("sql_parse trims and ignores empty") { + auto stmts = sql_parse("; ; SELECT 1;;"); + REQUIRE(stmts.size() == 1); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); +} + +TEST_CASE("sql_parse: ddl and dcl keywords") { + auto stmts = sql_parse("CREATE TABLE t(a); DROP TABLE t; ALTER TABLE t ADD b; UPDATE t SET a=1;"); + REQUIRE(stmts.size() == 4); + REQUIRE(stmts[0].kind == SqlStmtKind::Create); + REQUIRE(stmts[1].kind == SqlStmtKind::Drop); + REQUIRE(stmts[2].kind == SqlStmtKind::Alter); + REQUIRE(stmts[3].kind == SqlStmtKind::Update); +} + +TEST_CASE("sql_parse: pragma and explain") { + auto stmts = sql_parse("PRAGMA foreign_keys = ON; EXPLAIN SELECT 1;"); + REQUIRE(stmts.size() == 2); + REQUIRE(stmts[0].kind == SqlStmtKind::Pragma); + REQUIRE(stmts[1].kind == SqlStmtKind::Explain); +} + +TEST_CASE("sql_parse: WITH clasifica como Select") { + auto stmts = sql_parse("WITH x AS (SELECT 1) SELECT * FROM x;"); + REQUIRE(stmts.size() == 1); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); +} + +TEST_CASE("sql_parse: case-insensitive y trim") { + auto stmts = sql_parse(" select 1;\n INSERT INTO t VALUES (2);"); + REQUIRE(stmts.size() == 2); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); + REQUIRE(stmts[1].kind == SqlStmtKind::Insert); +} + +TEST_CASE("sql_parse: line tracking") { + auto stmts = sql_parse("\n\nSELECT 1;\n-- skip\nDELETE FROM t;\n"); + REQUIRE(stmts.size() == 2); + REQUIRE(stmts[0].line == 3); + REQUIRE(stmts[1].line == 5); +} + +TEST_CASE("sql_parse: ultimo statement sin ;") { + auto stmts = sql_parse("SELECT 1"); + REQUIRE(stmts.size() == 1); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); +} + +TEST_CASE("sql_classify: standalone") { + REQUIRE(sql_classify("SELECT 1") == SqlStmtKind::Select); + REQUIRE(sql_classify("garbage") == SqlStmtKind::Unknown); + REQUIRE(sql_classify("") == SqlStmtKind::Unknown); + REQUIRE(sql_classify("/* c */ DELETE FROM t") == SqlStmtKind::Delete); +} + +TEST_CASE("sql_parse: ; dentro de string no separa") { + auto stmts = sql_parse("SELECT 'a;b;c' FROM t;"); + REQUIRE(stmts.size() == 1); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); +} + +TEST_CASE("sql_parse: backtick identifier") { + auto stmts = sql_parse("SELECT * FROM `weird;name`;"); + REQUIRE(stmts.size() == 1); + REQUIRE(stmts[0].kind == SqlStmtKind::Select); +}