/* Extensiones nativas: instalación, permisos, comandos con historial, errores y persistencia. */ #include #include #include "tgb_test.h" #include "../src/extensions/ext_internal.h" #ifndef TGB_TEST_PLUGIN_DIR #error "define TGB_TEST_PLUGIN_DIR (lo hace tools/dev-build.sh)" #endif static tgb_context *g_ctx; static char g_root[200]; static char g_mark[400]; #define PLUGIN(name) TGB_TEST_PLUGIN_DIR "/" name ".so" #define ALL_CAPS (TGB_CAP_READ_DOCUMENT | TGB_CAP_EDIT_DOCUMENT | TGB_CAP_EVENTS) static tgb_document *new_doc(int w, int h) { tgb_document *d = NULL; CHECK_OK(tgb_document_create(g_ctx, w, h, 12, &d)); return d; } static size_t undo_count(const tgb_document *d) { tgb_history_stats s; tgb_history_get_stats(d, &s); return s.undo_count; } static size_t mark_count(const char *what) { FILE *f = fopen(g_mark, "r"); if (!f) return 0; char line[100]; size_t n = 0; while (fgets(line, sizeof line, f)) { line[strcspn(line, "\n")] = 0; if (!strcmp(line, what)) n++; } fclose(f); return n; } static size_t find_cmd(tgb_plugin_manager *m, const char *full) { for (size_t i = 0; i < tgb_plugin_command_count(m); i++) { char b[200]; tgb_plugin_command_name(m, i, b, sizeof b); if (!strcmp(b, full)) return i; } return (size_t)-1; } static void reset_world(void) { char p[600]; snprintf(p, sizeof p, "%s/config/extensions.json", g_root); remove(p); snprintf(p, sizeof p, "rm -rf '%s/data/extensions'", g_root); if (system(p)) {} remove(g_mark); } static void write_file(const char *path, const char *text) { FILE *f = fopen(path, "w"); fputs(text, f); fclose(f); } static int g_loaded_events, g_unloaded_events; static void on_event(const tgb_event *ev, void *u) { (void)u; if (ev->type == TGB_EVENT_PLUGIN_LOADED) g_loaded_events++; if (ev->type == TGB_EVENT_PLUGIN_UNLOADED) g_unloaded_events++; } TEST(install_never_runs_code) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_EQ(tgb_plugin_count(m), 0); size_t idx = 99; CHECK_STATUS(tgb_plugin_install(m, "/tmp/no-existe.so", "x", &idx), TGB_ERR_NOT_FOUND); CHECK_STATUS(tgb_plugin_install(m, PLUGIN("plug_good"), "../malo", &idx), TGB_ERR_INVALID_ARG); /* nada de rutas en el id */ CHECK_STATUS(tgb_plugin_install(m, PLUGIN("plug_good"), "Mayus", &idx), TGB_ERR_INVALID_ARG); CHECK_STATUS(tgb_plugin_install(m, PLUGIN("plug_good"), "", &idx), TGB_ERR_INVALID_ARG); CHECK_STATUS(tgb_plugin_install(m, PLUGIN("plug_good"), "-raro", &idx), TGB_ERR_INVALID_ARG); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_good"), "cruz", &idx)); CHECK_STATUS(tgb_plugin_install(m, PLUGIN("plug_good"), "cruz", &idx), TGB_ERR_EXISTS); CHECK_EQ(idx, 0); tgb_plugin_entry_info info; CHECK_OK(tgb_plugin_info_at(m, 0, &info)); CHECK(!strcmp(info.id, "cruz") && !strcmp(info.kind, "native") && !info.enabled && !info.loaded && info.granted_caps == 0); char p[600]; snprintf(p, sizeof p, "%s/data/extensions/cruz.so", g_root); CHECK(access(p, F_OK) == 0); /* copiada a la carpeta del usuario */ snprintf(p, sizeof p, "%s/config/extensions.json", g_root); CHECK(access(p, F_OK) == 0); CHECK_EQ(mark_count("init"), 0); /* instalar NO ejecuta nada de la extensión */ CHECK_EQ(tgb_plugin_command_count(m), 0); /* un archivo suelto en la carpeta, sin pasar por el registro, jamás se carga */ snprintf(p, sizeof p, "%s/data/extensions/intruso.so", g_root); CHECK_OK(0); FILE *src = fopen(PLUGIN("plug_good"), "rb"), *dst = fopen(p, "wb"); char buf[4096]; size_t n; while ((n = fread(buf, 1, sizeof buf, src)) > 0) fwrite(buf, 1, n, dst); fclose(src); fclose(dst); tgb_plugin_manager_destroy(m); CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_EQ(tgb_plugin_count(m), 1); CHECK_EQ(mark_count("init"), 0); tgb_plugin_manager_destroy(m); } TEST(enable_needs_granted_permissions) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_good"), "cruz", NULL)); CHECK_STATUS(tgb_plugin_enable(m, 0, TGB_CAP_READ_DOCUMENT), TGB_ERR_PLUGIN); /* pide leer+editar, solo se concede leer */ CHECK(strstr(tgb_last_error(), "permisos") != NULL); tgb_plugin_entry_info info; tgb_plugin_info_at(m, 0, &info); CHECK(!info.loaded && !info.enabled && strlen(info.last_error) > 5); CHECK_EQ(tgb_plugin_command_count(m), 0); /* sin comandos residuales */ CHECK_STATUS(tgb_plugin_enable(m, 0, 0x80), TGB_ERR_INVALID_ARG); CHECK_STATUS(tgb_plugin_enable(m, 5, ALL_CAPS), TGB_ERR_RANGE); uint32_t sub; g_loaded_events = g_unloaded_events = 0; CHECK_OK(tgb_events_subscribe(g_ctx, on_event, NULL, &sub)); CHECK_OK(tgb_plugin_enable(m, 0, TGB_CAP_READ_DOCUMENT | TGB_CAP_EDIT_DOCUMENT)); CHECK_STATUS(tgb_plugin_enable(m, 0, ALL_CAPS), TGB_ERR_STATE); /* ya habilitada */ tgb_plugin_info_at(m, 0, &info); CHECK(info.loaded && info.enabled && !strcmp(info.name, "Cruz de prueba") && !strcmp(info.version, "1.0.0")); CHECK(info.required_caps == (TGB_CAP_READ_DOCUMENT | TGB_CAP_EDIT_DOCUMENT)); CHECK_EQ(g_loaded_events, 1); CHECK_EQ(tgb_plugin_command_count(m), 6); char name[200]; CHECK_OK(tgb_plugin_command_name(m, 0, name, sizeof name)); CHECK(!strcmp(name, "cruz:dibujar-cruz")); CHECK_STATUS(tgb_plugin_command_name(m, 99, name, sizeof name), TGB_ERR_RANGE); CHECK_OK(tgb_plugin_disable(m, 0)); CHECK_EQ(g_unloaded_events, 1); CHECK_EQ(mark_count("shutdown"), 2); /* una al rechazarla por falta de permisos (su init ya había corrido) y otra al deshabilitarla */ CHECK_OK(tgb_events_unsubscribe(g_ctx, sub)); tgb_plugin_manager_destroy(m); } TEST(commands_are_one_undoable_operation) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_good"), "cruz", NULL)); CHECK_OK(tgb_plugin_enable(m, 0, ALL_CAPS)); tgb_document *d = new_doc(32, 32); size_t n0 = undo_count(d); size_t cross = find_cmd(m, "cruz:dibujar-cruz"); CHECK_OK(tgb_plugin_run_command(m, cross, d)); CHECK_EQ(undo_count(d), n0 + 1); CHECK_EQ(count_alpha(tgb_frame_cel(d, 0, 0)), 13); CHECK(is_px(tgb_frame_cel(d, 0, 0), 16, 16, RED)); CHECK(strstr(tgb_history_undo_label(d), "dibujar-cruz") != NULL); CHECK_OK(tgb_history_undo(d)); CHECK_EQ(px_at(d, 0, 0, 16, 16).a, 0); CHECK_OK(tgb_history_redo(d)); /* estructura + píxeles = UNA sola operación */ size_t n1 = undo_count(d); CHECK_OK(tgb_plugin_run_command(m, find_cmd(m, "cruz:dos-pasos"), d)); CHECK_EQ(undo_count(d), n1 + 1); CHECK_EQ(tgb_frame_count(d), 2); CHECK(is_px(tgb_frame_cel(d, 0, 0), 1, 1, RED)); CHECK_OK(tgb_history_undo(d)); CHECK_EQ(tgb_frame_count(d), 1); CHECK_EQ(px_at(d, 0, 0, 1, 1).a, 0); CHECK(is_px(tgb_frame_cel(d, 0, 0), 16, 16, RED)); /* lo anterior sigue ahí */ /* lectura */ CHECK_STATUS(tgb_plugin_run_command(m, find_cmd(m, "cruz:lee-pixel"), d), TGB_OK); CHECK_EQ(undo_count(d), n1); /* un comando sin cambios no ensucia el historial */ /* argumentos inválidos */ CHECK_STATUS(tgb_plugin_run_command(m, 99, d), TGB_ERR_RANGE); CHECK_STATUS(tgb_plugin_run_command(m, cross, NULL), TGB_ERR_INVALID_ARG); CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "x")); CHECK_STATUS(tgb_plugin_run_command(m, cross, d), TGB_ERR_STATE); /* no se ejecuta con una edición abierta */ CHECK_OK(tgb_edit_cancel(d)); tgb_document_destroy(d); tgb_plugin_manager_destroy(m); } TEST(failing_command_restores_document) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_good"), "cruz", NULL)); CHECK_OK(tgb_plugin_enable(m, 0, ALL_CAPS)); tgb_document *d = new_doc(32, 32); CHECK_OK(paint_px(d, 0, 0, 9, 9, BLUE)); size_t n0 = undo_count(d); CHECK_STATUS(tgb_plugin_run_command(m, find_cmd(m, "cruz:falla-a-medias"), d), TGB_ERR_PLUGIN); CHECK(strstr(tgb_last_error(), "Se deshicieron todos sus cambios") != NULL); CHECK_EQ(tgb_frame_count(d), 1); /* el fotograma que añadió ya no está */ CHECK_EQ(px_at(d, 0, 0, 2, 2).a, 0); /* ni su píxel */ CHECK(is_px(tgb_frame_cel(d, 0, 0), 9, 9, BLUE)); /* lo anterior intacto */ CHECK_EQ(undo_count(d), n0); /* no deja rastro en el historial */ CHECK(tgb_document_is_consistent(d) && !tgb_edit_active(d)); /* edición sin cerrar: se considera fallo y se revierte */ CHECK_STATUS(tgb_plugin_run_command(m, find_cmd(m, "cruz:olvida-cerrar"), d), TGB_ERR_PLUGIN); CHECK(strstr(tgb_last_error(), "edit_end") != NULL); CHECK_EQ(px_at(d, 0, 0, 3, 3).a, 0); CHECK(!tgb_edit_active(d)); CHECK_EQ(undo_count(d), n0); /* el documento sigue plenamente utilizable */ CHECK_OK(paint_px(d, 0, 0, 4, 4, GREEN)); CHECK_OK(tgb_history_undo(d)); tgb_document_destroy(d); tgb_plugin_manager_destroy(m); } TEST(permissions_are_enforced_by_the_host) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_readonly"), "lector", NULL)); CHECK_OK(tgb_plugin_enable(m, 0, TGB_CAP_READ_DOCUMENT)); /* solo lectura concedida */ tgb_document *d = new_doc(16, 16); size_t n0 = undo_count(d); CHECK_STATUS(tgb_plugin_run_command(m, find_cmd(m, "lector:intenta-editar"), d), TGB_ERR_PLUGIN); CHECK(strstr(tgb_last_error(), "permiso") != NULL); CHECK_STATUS(tgb_plugin_run_command(m, find_cmd(m, "lector:intenta-anadir"), d), TGB_ERR_PLUGIN); CHECK_EQ(tgb_frame_count(d), 1); CHECK_EQ(undo_count(d), n0); tgb_document_destroy(d); tgb_plugin_manager_destroy(m); } TEST(bad_extensions_are_refused_cleanly) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); struct { const char *file, *id; tgb_status expected; const char *words; } bad[] = { {"plug_needs_edit", "pide", TGB_ERR_PLUGIN, "permisos"}, {"plug_init_fails", "falla", TGB_ERR_PLUGIN, "falló al iniciarse"}, {"plug_no_entry", "sinentrada", TGB_ERR_PLUGIN, "tgb_plugin_init"}, {"plug_old_api", "vieja", TGB_ERR_INCOMPATIBLE, "API"}, {"plug_no_version", "sinversion", TGB_ERR_INCOMPATIBLE, "tgb_plugin_api_version"}, {"plug_no_name", "sinnombre", TGB_ERR_PLUGIN, "nombre"}, }; for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) { size_t idx; char src[600]; snprintf(src, sizeof src, "%s/%s.so", TGB_TEST_PLUGIN_DIR, bad[i].file); CHECK_OK(tgb_plugin_install(m, src, bad[i].id, &idx)); tgb_status s = tgb_plugin_enable(m, idx, TGB_CAP_READ_DOCUMENT); if (s != bad[i].expected) printf(" %s: se esperaba %s y devolvió %s (%s)\n", bad[i].file, tgb_status_name(bad[i].expected), tgb_status_name(s), tgb_last_error()); CHECK_EQ(s, bad[i].expected); CHECK(strstr(tgb_last_error(), bad[i].words) != NULL); tgb_plugin_entry_info info; tgb_plugin_info_at(m, idx, &info); CHECK(!info.loaded && !info.enabled && strlen(info.last_error) > 3); } CHECK_EQ(tgb_plugin_command_count(m), 0); CHECK_EQ(mark_count("shutdown-needs-edit"), 1); /* la que ya se había iniciado se cierra bien */ /* el archivo desapareció del disco */ char p[600]; snprintf(p, sizeof p, "%s/data/extensions/vieja.so", g_root); remove(p); CHECK_STATUS(tgb_plugin_enable(m, 3, ALL_CAPS), TGB_ERR_NOT_FOUND); tgb_plugin_manager_destroy(m); } TEST(disable_remove_and_indexes) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_good"), "uno", NULL)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_readonly"), "dos", NULL)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_events"), "tres", NULL)); CHECK_OK(tgb_plugin_enable(m, 0, ALL_CAPS)); CHECK_OK(tgb_plugin_enable(m, 1, TGB_CAP_READ_DOCUMENT)); CHECK_OK(tgb_plugin_enable(m, 2, ALL_CAPS)); CHECK_EQ(tgb_plugin_command_count(m), 6 + 2 + 1); tgb_document *d = new_doc(32, 32); CHECK_OK(tgb_plugin_run_command_named(m, "uno:dibujar-cruz", d)); /* por nombre estable */ CHECK_OK(tgb_history_undo(d)); CHECK_OK(tgb_plugin_disable(m, 0)); CHECK_EQ(tgb_plugin_command_count(m), 3); CHECK_EQ(find_cmd(m, "uno:dibujar-cruz"), (size_t)-1); /* el nombre de una extensión deshabilitada NO ejecuta nada (y no ejecuta el comando "de al lado") */ CHECK_STATUS(tgb_plugin_run_command_named(m, "uno:dibujar-cruz", d), TGB_ERR_NOT_FOUND); CHECK_STATUS(tgb_plugin_run_command_named(m, "uno", d), TGB_ERR_NOT_FOUND); CHECK_STATUS(tgb_plugin_run_command_named(m, NULL, d), TGB_ERR_INVALID_ARG); CHECK_STATUS(tgb_plugin_run_command(m, 99, d), TGB_ERR_RANGE); CHECK_OK(tgb_plugin_enable(m, 0, ALL_CAPS)); /* se puede volver a habilitar */ CHECK_OK(tgb_plugin_run_command(m, find_cmd(m, "uno:dibujar-cruz"), d)); CHECK_OK(tgb_plugin_remove(m, 1)); /* quitar la del medio */ CHECK_EQ(tgb_plugin_count(m), 2); tgb_plugin_entry_info info; tgb_plugin_info_at(m, 1, &info); CHECK(!strcmp(info.id, "tres") && info.loaded); char p[600]; snprintf(p, sizeof p, "%s/data/extensions/dos.so", g_root); CHECK(access(p, F_OK) != 0); /* el archivo se borró */ CHECK_OK(tgb_history_undo(d)); CHECK_OK(tgb_plugin_run_command(m, find_cmd(m, "uno:dibujar-cruz"), d)); /* los índices siguen bien tras eliminar */ CHECK(find_cmd(m, "tres:eventos-vistos") != (size_t)-1); CHECK_STATUS(tgb_plugin_remove(m, 9), TGB_ERR_RANGE); tgb_document_destroy(d); tgb_plugin_manager_destroy(m); CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); /* el registro quedó coherente en disco */ CHECK_EQ(tgb_plugin_count(m), 2); tgb_plugin_info_at(m, 0, &info); CHECK(!strcmp(info.id, "uno")); tgb_plugin_manager_destroy(m); } TEST(registry_persistence_safe_mode_and_corruption) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_good"), "cruz", NULL)); CHECK_OK(tgb_plugin_enable(m, 0, ALL_CAPS)); tgb_plugin_manager_destroy(m); CHECK_EQ(mark_count("shutdown"), 1); /* al reiniciar se vuelve a cargar lo que el usuario había habilitado, con sus permisos */ CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); tgb_plugin_entry_info info; tgb_plugin_info_at(m, 0, &info); CHECK(info.enabled && info.loaded && info.granted_caps == ALL_CAPS); CHECK_EQ(tgb_plugin_command_count(m), 6); tgb_plugin_manager_destroy(m); /* modo seguro: no se carga NADA, pero se conserva la decisión del usuario */ size_t before = mark_count("init"); setenv("TGB_SAFE_MODE", "1", 1); CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); tgb_plugin_info_at(m, 0, &info); CHECK(info.enabled && !info.loaded); CHECK_EQ(mark_count("init"), before); CHECK_EQ(tgb_plugin_command_count(m), 0); tgb_plugin_manager_destroy(m); unsetenv("TGB_SAFE_MODE"); /* entradas peligrosas del registro se ignoran: nunca se carga una ruta arbitraria */ char reg[600]; snprintf(reg, sizeof reg, "%s/config/extensions.json", g_root); write_file(reg, "{\"version\":1,\"plugins\":[{\"id\":\"../../etc/algo\",\"enabled\":true},{\"id\":\"cruz\",\"enabled\":false,\"granted_caps\":3},{\"id\":\"cruz\",\"enabled\":true}]}"); CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_EQ(tgb_plugin_count(m), 1); /* la ruta con '..' y el duplicado se descartan */ tgb_plugin_info_at(m, 0, &info); CHECK(!info.enabled && !info.loaded); tgb_plugin_manager_destroy(m); /* registro dañado: se avisa y NO se sobrescribe */ write_file(reg, "{esto no es json"); CHECK_STATUS(tgb_plugin_manager_create(g_ctx, &m), TGB_ERR_FORMAT); CHECK(m == NULL && strstr(tgb_last_error(), "extensions.json") != NULL); FILE *f = fopen(reg, "r"); char line[100] = ""; if (fgets(line, sizeof line, f)) {} fclose(f); CHECK(!strcmp(line, "{esto no es json")); } TEST(extension_receives_events_and_is_unsubscribed_on_disable) { reset_world(); tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); CHECK_OK(tgb_plugin_install(m, PLUGIN("plug_events"), "oyente", NULL)); CHECK_STATUS(tgb_plugin_enable(m, 0, TGB_CAP_READ_DOCUMENT), TGB_ERR_PLUGIN); /* sin permiso de eventos no puede suscribirse */ CHECK_OK(tgb_plugin_enable(m, 0, ALL_CAPS)); tgb_document *d = new_doc(16, 16); size_t cmd = find_cmd(m, "oyente:eventos-vistos"); CHECK_STATUS(tgb_plugin_run_command(m, cmd, d), TGB_ERR_PLUGIN); /* aún no vio eventos de píxeles */ CHECK_OK(paint_px(d, 0, 0, 1, 1, RED)); CHECK_OK(tgb_plugin_run_command(m, cmd, d)); /* ahora sí */ CHECK_OK(tgb_plugin_disable(m, 0)); CHECK_OK(paint_px(d, 0, 0, 2, 2, RED)); /* si la suscripción siguiera viva se llamaría a código ya descargado: el programa caería */ CHECK_OK(tgb_plugin_enable(m, 0, ALL_CAPS)); CHECK_OK(paint_px(d, 0, 0, 3, 3, RED)); tgb_document_destroy(d); tgb_plugin_manager_destroy(m); tgb_document *after = new_doc(4, 4); /* tras destruir el gestor tampoco queda ninguna suscripción */ CHECK_OK(paint_px(after, 0, 0, 0, 0, RED)); tgb_document_destroy(after); } TEST(lua_extensions_without_lua_support) { reset_world(); if (tgb_lua_available()) return; /* con Lua compilado se prueban en extensions/lua */ tgb_plugin_manager *m = NULL; CHECK_OK(tgb_plugin_manager_create(g_ctx, &m)); char src[600]; snprintf(src, sizeof src, "%s/script.lua", g_root); write_file(src, "info = {name='x', version='1', caps={}}\n"); size_t idx; CHECK_OK(tgb_plugin_install(m, src, "script", &idx)); tgb_plugin_entry_info info; tgb_plugin_info_at(m, idx, &info); CHECK(!strcmp(info.kind, "lua") && !strcmp(info.file, "script.lua")); CHECK_STATUS(tgb_plugin_enable(m, idx, TGB_CAP_READ_DOCUMENT), TGB_ERR_UNSUPPORTED); CHECK(strstr(tgb_last_error(), "TAHGIBU_WITH_LUA") != NULL); tgb_plugin_info_at(m, idx, &info); CHECK(!info.loaded && !info.enabled); tgb_plugin_manager_destroy(m); } int main(void) { g_ctx = tgb_test_context(g_root, sizeof g_root); snprintf(g_mark, sizeof g_mark, "%s/marca.txt", g_root); setenv("TGB_TEST_MARK", g_mark, 1); RUN(install_never_runs_code); RUN(enable_needs_granted_permissions); RUN(commands_are_one_undoable_operation); RUN(failing_command_restores_document); RUN(permissions_are_enforced_by_the_host); RUN(bad_extensions_are_refused_cleanly); RUN(disable_remove_and_indexes); RUN(registry_persistence_safe_mode_and_corruption); RUN(extension_receives_events_and_is_unsubscribed_on_disable); RUN(lua_extensions_without_lua_support); tgb_context_destroy(g_ctx); tgb_test_rmtree(g_root); TEST_SUMMARY(); }