test(cpp): tests para sql_parse, process_state_machine, file_poll_diff

This commit is contained in:
2026-04-28 23:58:40 +02:00
parent 0cfaa27ee1
commit 4f280f34d7
5 changed files with 302 additions and 6 deletions
+13 -6
View File
@@ -101,18 +101,25 @@ std::vector<SqlStatement> sql_parse(const std::string& input) {
switch (m) {
case Mode::Normal:
if (!stmt_has_content && !std::isspace(static_cast<unsigned char>(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<unsigned char>(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 == '"') {
+8
View File
@@ -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)
+105
View File
@@ -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 <algorithm>
#include <vector>
#include <string>
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<FileEntry> before = {{"a", 10, 1}, {"b", 20, 2}, {"c", 30, 3}};
std::vector<FileEntry> 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<std::string>{"d"});
REQUIRE(d.modified == std::vector<std::string>{"b"});
REQUIRE(d.removed == std::vector<std::string>{"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<FileEntry> 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<FileEntry> after = {{"a", 1, 1}, {"b", 2, 2}};
auto d = file_poll_diff({}, after);
sort_diff(d);
REQUIRE(d.added == std::vector<std::string>{"a", "b"});
REQUIRE(d.modified.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: all removed (after vacio)") {
std::vector<FileEntry> before = {{"a", 1, 1}, {"b", 2, 2}};
auto d = file_poll_diff(before, {});
sort_diff(d);
REQUIRE(d.removed == std::vector<std::string>{"a", "b"});
REQUIRE(d.modified.empty());
REQUIRE(d.added.empty());
}
TEST_CASE("file_poll_diff: solo size cambia -> modified") {
std::vector<FileEntry> before = {{"a", 100, 5}};
std::vector<FileEntry> after = {{"a", 200, 5}};
auto d = file_poll_diff(before, after);
REQUIRE(d.modified == std::vector<std::string>{"a"});
REQUIRE(d.added.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: solo mtime cambia -> modified") {
std::vector<FileEntry> before = {{"a", 100, 5}};
std::vector<FileEntry> after = {{"a", 100, 9}};
auto d = file_poll_diff(before, after);
REQUIRE(d.modified == std::vector<std::string>{"a"});
REQUIRE(d.added.empty());
REQUIRE(d.removed.empty());
}
TEST_CASE("file_poll_diff: combinacion compleja") {
std::vector<FileEntry> before = {
{"keep", 10, 1},
{"mod_size", 20, 2},
{"mod_mtime", 30, 3},
{"removed", 40, 4},
};
std::vector<FileEntry> 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<std::string>{"new"});
REQUIRE(d.modified == std::vector<std::string>{"mod_mtime", "mod_size"});
REQUIRE(d.removed == std::vector<std::string>{"removed"});
}
+83
View File
@@ -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 <cstring>
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);
}
+93
View File
@@ -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);
}