Files
fn_registry/cpp/tests/test_layout_storage.cpp
T
egutierrez 401d8523b4 feat(infra): auto-commit con 11 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 13:30:27 +02:00

233 lines
8.2 KiB
C++

// E2E tests for layout_storage last_active persistence.
//
// Cubre:
// - set/get last_active sobreviven al cierre y reapertura del handle.
// - get_last_active retorna "" si la fila apunta a un layout que ya no existe
// (referencia colgante).
// - on_apply / on_delete (en make_callbacks) actualizan el meta correctamente.
// - on_reset borra el last_active.
// - apply_pending no rompe el flujo cuando solo hay set/get sin ImGui.
//
// Importante: NO se prueba on_save ni layout_storage_save aqui — requieren un
// ImGui context vivo (SaveIniSettingsToMemory). Esa parte queda cubierta
// indirectamente cuando una app real arranca con run_app. En este test,
// "creamos" layouts insertando directamente en imgui_layouts via SQLite.
#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"
#include "core/layout_storage.h"
#include <sqlite3.h>
#include <cstdio>
#include <filesystem>
#include <string>
namespace {
std::string tmp_db_path(const char* tag) {
auto dir = std::filesystem::temp_directory_path() /
(std::string("fn_layout_storage_") + tag);
std::error_code ec;
std::filesystem::create_directories(dir, ec);
return (dir / "layouts.db").string();
}
// Inserta un layout "vacio" directamente para no depender de ImGui.
bool seed_layout(const std::string& db_path, const char* name) {
sqlite3* db = nullptr;
if (sqlite3_open(db_path.c_str(), &db) != SQLITE_OK) return false;
sqlite3_exec(db,
"CREATE TABLE IF NOT EXISTS imgui_layouts ("
" name TEXT PRIMARY KEY, ini TEXT NOT NULL, updated_at INTEGER NOT NULL"
");",
nullptr, nullptr, nullptr);
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(db,
"INSERT INTO imgui_layouts (name, ini, updated_at) "
"VALUES (?, '[Window][Debug]\n', strftime('%s','now')) "
"ON CONFLICT(name) DO NOTHING;",
-1, &stmt, nullptr);
if (rc != SQLITE_OK) { sqlite3_close(db); return false; }
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
sqlite3_close(db);
return rc == SQLITE_DONE;
}
bool delete_row(const std::string& db_path, const char* name) {
sqlite3* db = nullptr;
if (sqlite3_open(db_path.c_str(), &db) != SQLITE_OK) return false;
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(db,
"DELETE FROM imgui_layouts WHERE name = ?;", -1, &stmt, nullptr);
if (rc != SQLITE_OK) { sqlite3_close(db); return false; }
sqlite3_bind_text(stmt, 1, name, -1, SQLITE_TRANSIENT);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
sqlite3_close(db);
return rc == SQLITE_DONE;
}
} // namespace
TEST_CASE("layout_storage: last_active survives reopen", "[layout_storage]") {
auto path = tmp_db_path("survives_reopen");
std::filesystem::remove(path);
REQUIRE(seed_layout(path, "Coding"));
REQUIRE(seed_layout(path, "Debug"));
// sesion 1: marca last_active
{
auto* s = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s != nullptr);
REQUIRE(fn_ui::layout_storage_set_last_active(s, "Coding"));
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Coding");
fn_ui::layout_storage_close(s);
}
// sesion 2: el meta sobrevive
{
auto* s = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s != nullptr);
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Coding");
// sobreescribir
REQUIRE(fn_ui::layout_storage_set_last_active(s, "Debug"));
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Debug");
// limpiar
REQUIRE(fn_ui::layout_storage_set_last_active(s, ""));
REQUIRE(fn_ui::layout_storage_get_last_active(s).empty());
fn_ui::layout_storage_close(s);
}
}
TEST_CASE("layout_storage: get_last_active ignora referencia colgante",
"[layout_storage]") {
auto path = tmp_db_path("dangling");
std::filesystem::remove(path);
REQUIRE(seed_layout(path, "Coding"));
auto* s = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s != nullptr);
REQUIRE(fn_ui::layout_storage_set_last_active(s, "Coding"));
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Coding");
// Borrar el layout sin pasar por la API (simula DB editada externamente).
fn_ui::layout_storage_close(s);
REQUIRE(delete_row(path, "Coding"));
auto* s2 = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s2 != nullptr);
// Sobrevive el meta pero apunta a un layout inexistente → "".
REQUIRE(fn_ui::layout_storage_get_last_active(s2).empty());
fn_ui::layout_storage_close(s2);
}
TEST_CASE("layout_storage: callbacks on_apply persiste last_active",
"[layout_storage]") {
auto path = tmp_db_path("cb_apply");
std::filesystem::remove(path);
REQUIRE(seed_layout(path, "Default"));
REQUIRE(seed_layout(path, "Wide"));
auto* s = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s != nullptr);
fn_ui::LayoutCallbacks cb;
fn_ui::layout_storage_make_callbacks(s, cb);
// on_apply NO requiere ImGui (solo deja blob pendiente + set meta).
cb.on_apply("Wide");
REQUIRE(cb.active_name == "Wide");
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Wide");
fn_ui::layout_storage_close(s);
// Reopen — last_active persiste.
auto* s2 = fn_ui::layout_storage_open(path.c_str());
REQUIRE(fn_ui::layout_storage_get_last_active(s2) == "Wide");
fn_ui::layout_storage_close(s2);
}
TEST_CASE("layout_storage: callbacks on_delete del activo limpia last_active",
"[layout_storage]") {
auto path = tmp_db_path("cb_delete");
std::filesystem::remove(path);
REQUIRE(seed_layout(path, "A"));
REQUIRE(seed_layout(path, "B"));
auto* s = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s != nullptr);
fn_ui::LayoutCallbacks cb;
fn_ui::layout_storage_make_callbacks(s, cb);
cb.on_apply("A");
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "A");
// Borrar otro: no toca last_active.
cb.on_delete("B");
REQUIRE(cb.active_name == "A");
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "A");
// Borrar el activo: limpia.
cb.on_delete("A");
REQUIRE(cb.active_name.empty());
REQUIRE(fn_ui::layout_storage_get_last_active(s).empty());
fn_ui::layout_storage_close(s);
}
TEST_CASE("layout_storage: callbacks on_reset limpia last_active",
"[layout_storage]") {
auto path = tmp_db_path("cb_reset");
std::filesystem::remove(path);
REQUIRE(seed_layout(path, "X"));
auto* s = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s != nullptr);
fn_ui::LayoutCallbacks cb;
fn_ui::layout_storage_make_callbacks(s, cb);
cb.on_apply("X");
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "X");
// on_reset toca ImGui::LoadIniSettingsFromMemory — sin context cascaria.
// Para este test verificamos solo el side-effect en BD pasando por el
// mismo path: set_last_active("") es lo que el reset hace.
REQUIRE(fn_ui::layout_storage_set_last_active(s, ""));
REQUIRE(fn_ui::layout_storage_get_last_active(s).empty());
fn_ui::layout_storage_close(s);
}
TEST_CASE("layout_storage: open con BD legacy (sin layout_meta) no rompe",
"[layout_storage]") {
auto path = tmp_db_path("legacy");
std::filesystem::remove(path);
// Crear BD con SOLO la tabla vieja imgui_layouts (sin layout_meta).
{
sqlite3* db = nullptr;
REQUIRE(sqlite3_open(path.c_str(), &db) == SQLITE_OK);
REQUIRE(sqlite3_exec(db,
"CREATE TABLE imgui_layouts ("
" name TEXT PRIMARY KEY, ini TEXT NOT NULL, updated_at INTEGER NOT NULL"
");"
"INSERT INTO imgui_layouts VALUES ('Old', '[X]', 1);",
nullptr, nullptr, nullptr) == SQLITE_OK);
sqlite3_close(db);
}
// Open debe crear layout_meta on-the-fly via CREATE TABLE IF NOT EXISTS.
auto* s = fn_ui::layout_storage_open(path.c_str());
REQUIRE(s != nullptr);
REQUIRE(fn_ui::layout_storage_get_last_active(s).empty());
REQUIRE(fn_ui::layout_storage_set_last_active(s, "Old"));
REQUIRE(fn_ui::layout_storage_get_last_active(s) == "Old");
fn_ui::layout_storage_close(s);
}