Files
Tahgibu/tests/test_plugins.c
T
2026-10-24 12:01:52 -03:00

379 lines
19 KiB
C

/* Extensiones nativas: instalación, permisos, comandos con historial, errores y persistencia. */
#include <stdlib.h>
#include <sys/stat.h>
#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();
}