This commit is contained in:
Clinv committed 2026-10-24 12:01:52 -03:00
1 parent 7f7fcd545d
commit 4d9a339421
114 files changed
+11383 -1837

No files matched your search

-101
View File
@@ -1,101 +0,0 @@
// Pruebas del núcleo. No usan raylib.
#include <cmath>
#include <cstdio>
#include <limits>
#include <stdexcept>
#include "core/Constants.hpp"
#include "core/Frame.hpp"
#include "core/Project.hpp"
namespace {
int g_failures = 0;
void check(bool condition, const char* expression, int line) {
if (!condition) {
std::fprintf(stderr, "FALLO (línea %d): %s\n", line, expression);
++g_failures;
}
}
bool near(double a, double b) {
return std::fabs(a - b) < 1e-9;
}
} // namespace
#define CHECK(expr) check((expr), #expr, __LINE__)
int main() {
using namespace tahgibu;
// --- Límite de FPS -----------------------------------------------------
CHECK(Frame::clampFps(120.0f) == kMaxFps);
CHECK(Frame::clampFps(60.0f) == 60.0f);
CHECK(Frame::clampFps(1.0f) == 1.0f);
CHECK(Frame::clampFps(0.0f) == kMinFps);
CHECK(Frame::clampFps(-5.0f) == kMinFps);
CHECK(Frame::clampFps(std::numeric_limits<float>::quiet_NaN()) == kDefaultFps);
CHECK(Frame::clampFps(std::numeric_limits<float>::infinity()) == kDefaultFps);
// --- Frame ---------------------------------------------------------------
Frame frame;
CHECK(!frame.hasFpsOverride());
CHECK(near(frame.durationSeconds(24.0f), 1.0 / 24.0));
frame.setFpsOverride(1.0f);
CHECK(frame.hasFpsOverride());
CHECK(near(frame.durationSeconds(60.0f), 1.0));
frame.setFpsOverride(1000.0f); // nunca por encima de 60
CHECK(frame.effectiveFps(24.0f) == kMaxFps);
frame.clearFpsOverride();
CHECK(!frame.hasFpsOverride());
CHECK(frame.effectiveFps(30.0f) == 30.0f);
CHECK(frame.effectiveFps(500.0f) == kMaxFps); // la base también se limita
// --- Project -------------------------------------------------------------
Project project;
CHECK(project.frameCount() == 1);
CHECK(project.baseFps() == kDefaultFps);
project.setBaseFps(999.0f);
CHECK(project.baseFps() == kMaxFps);
project.setBaseFps(std::numeric_limits<float>::quiet_NaN());
CHECK(project.baseFps() == kDefaultFps);
// Ejemplo de la especificación: A(60) B(60) C(1) D(60)
Project example;
example.setBaseFps(60.0f);
example.addFrame(); // B
const std::size_t c = example.addFrame(); // C
example.addFrame(); // D
example.frame(c).setFpsOverride(1.0f);
CHECK(example.frameCount() == 4);
CHECK(near(example.frameDurationSeconds(0), 1.0 / 60.0));
CHECK(near(example.frameDurationSeconds(c), 1.0));
CHECK(near(example.totalDurationSeconds(), 3.0 / 60.0 + 1.0));
// Cambiar el FPS base afecta a los frames sin FPS propio, pero no al de C.
example.setBaseFps(30.0f);
CHECK(near(example.frameDurationSeconds(0), 1.0 / 30.0));
CHECK(near(example.frameDurationSeconds(c), 1.0));
bool threw = false;
try {
example.frame(99);
} catch (const std::out_of_range&) {
threw = true;
}
CHECK(threw);
if (g_failures == 0) {
std::puts("OK: todas las pruebas del núcleo pasaron.");
return 0;
}
std::fprintf(stderr, "%d prueba(s) fallaron.\n", g_failures);
return 1;
}
+35
View File
@@ -0,0 +1,35 @@
/* GENERADO por tools/gen_png_fixtures.py con Pillow: no editar a mano. */
#ifndef TGB_FIXTURES_PNG_H
#define TGB_FIXTURES_PNG_H
#include <stddef.h>
#include <stdint.h>
#define FIX_W 4
#define FIX_H 3
static const uint8_t fix_rgba8[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,8,6,0,0,0,180,244,174,198,0,0,0,43,73,68,65,84,120,156,69,199,161,13,0,32,12,0,176,146,236,172,241,205,62,197,112,209,44,8,4,117,29,108,169,79,106,169,103,40,199,51,177,66,253,192,5,51,235,9,107,80,244,30,89,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_rgba8_px[] = {0,0,0,0,60,0,190,255,120,0,180,255,180,0,170,60,0,100,200,255,60,100,190,255,120,100,180,60,180,100,170,255,0,200,200,255,60,200,190,60,120,200,180,255,180,200,170,255};
static const uint8_t fix_rgb8[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,8,2,0,0,0,59,150,57,145,0,0,0,26,73,68,65,84,120,156,99,100,96,96,48,98,16,132,32,22,134,40,65,6,6,40,66,225,0,0,44,82,2,15,131,0,81,21,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_rgb8_px[] = {0,0,0,255,50,0,17,255,100,0,34,255,150,0,51,255,0,90,17,255,50,90,34,255,100,90,51,255,150,90,68,255,0,180,34,255,50,180,51,255,100,180,68,255,150,180,85,255};
static const uint8_t fix_gray8[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,8,0,0,0,0,145,159,241,26,0,0,0,23,73,68,65,84,120,156,99,100,208,208,208,96,226,226,226,226,98,226,226,226,226,2,0,7,108,0,206,40,183,200,126,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_gray8_px[] = {0,0,0,255,40,40,40,255,80,80,80,255,120,120,120,255,10,10,10,255,50,50,50,255,90,90,90,255,130,130,130,255,20,20,20,255,60,60,60,255,100,100,100,255,140,140,140,255};
static const uint8_t fix_graya8[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,8,4,0,0,0,30,253,102,77,0,0,0,27,73,68,65,84,120,156,99,100,253,47,199,32,199,32,199,192,194,48,135,129,129,129,129,1,193,0,0,47,203,2,160,83,77,14,190,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_graya8_px[] = {5,5,5,255,35,35,35,255,65,65,65,255,95,95,95,255,5,5,5,155,35,35,35,155,65,65,65,155,95,95,95,155,5,5,5,55,35,35,35,55,65,65,65,55,95,95,95,55};
static const uint8_t fix_palette_trns[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,8,3,0,0,0,131,42,94,244,0,0,3,0,80,76,84,69,0,255,0,60,195,20,120,135,40,180,75,60,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,159,71,4,107,0,0,0,4,116,82,78,83,255,128,0,255,161,161,148,102,0,0,0,17,73,68,65,84,120,156,99,96,96,100,98,6,99,6,16,13,0,0,149,0,19,22,208,45,139,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_palette_trns_px[] = {0,255,0,255,60,195,20,128,0,0,0,0,180,75,60,255,60,195,20,128,0,0,0,0,180,75,60,255,0,255,0,255,0,0,0,0,180,75,60,255,0,255,0,255,60,195,20,128};
static const uint8_t fix_bilevel[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,1,0,0,0,0,156,143,147,107,0,0,0,14,73,68,65,84,120,156,99,8,96,10,96,8,0,0,2,222,0,243,147,161,216,28,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_bilevel_px[] = {0,0,0,255,255,255,255,255,0,0,0,255,255,255,255,255,255,255,255,255,0,0,0,255,255,255,255,255,0,0,0,255,0,0,0,255,255,255,255,255,0,0,0,255,255,255,255,255};
static const uint8_t fix_palette2bit[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,2,3,0,0,0,201,154,70,85,0,0,0,12,80,76,84,69,255,0,0,0,255,0,0,0,255,255,255,0,214,2,143,123,0,0,0,14,73,68,65,84,120,156,99,80,98,10,101,18,4,0,1,204,0,141,255,92,63,121,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_palette2bit_px[] = {255,0,0,255,0,0,255,255,255,0,0,255,0,0,255,255,0,255,0,255,255,255,0,255,0,255,0,255,255,255,0,255,0,0,255,255,255,0,0,255,0,0,255,255,255,0,0,255};
static const uint8_t fix_gray16[] = {137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,4,0,0,0,3,16,0,0,0,0,193,15,45,89,0,0,0,28,73,68,65,84,120,156,99,96,96,224,95,32,239,160,247,128,69,89,131,95,67,64,67,64,3,206,0,0,61,151,4,9,163,240,125,70,0,0,0,0,73,69,78,68,174,66,96,130};
static const uint8_t fix_gray16_px[] = {0,0,0,255,15,15,15,255,31,31,31,255,46,46,46,255,35,35,35,255,50,50,50,255,66,66,66,255,82,82,82,255,70,70,70,255,85,85,85,255,101,101,101,255,117,117,117,255};
typedef struct { const char *name; const uint8_t *png; size_t len; const uint8_t *px; } png_fixture;
static const png_fixture png_fixtures[] = {
{"rgba8", fix_rgba8, sizeof fix_rgba8, fix_rgba8_px},
{"rgb8", fix_rgb8, sizeof fix_rgb8, fix_rgb8_px},
{"gray8", fix_gray8, sizeof fix_gray8, fix_gray8_px},
{"graya8", fix_graya8, sizeof fix_graya8, fix_graya8_px},
{"palette_trns", fix_palette_trns, sizeof fix_palette_trns, fix_palette_trns_px},
{"bilevel", fix_bilevel, sizeof fix_bilevel, fix_bilevel_px},
{"palette2bit", fix_palette2bit, sizeof fix_palette2bit, fix_palette2bit_px},
{"gray16", fix_gray16, sizeof fix_gray16, fix_gray16_px},
};
#endif
+14
View File
@@ -0,0 +1,14 @@
/* Ayudas comunes de las extensiones de PRUEBA. */
#ifndef PLUG_COMMON_H
#define PLUG_COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include "tahgibu/plugin.h"
static __attribute__((unused)) void mark(const char *what) { /* deja constancia en un archivo para que la prueba compruebe qué se ejecutó */
const char *p = getenv("TGB_TEST_MARK");
if (!p) return;
FILE *f = fopen(p, "a");
if (f) { fputs(what, f); fputc('\n', f); fclose(f); }
}
#endif
+14
View File
@@ -0,0 +1,14 @@
#include "plug_common.h"
TGB_PLUGIN_DECLARE_API_VERSION;
static const tgb_host_api *H;
static int seen_pixels = 0, seen_total = 0;
static void on_event(const tgb_event *ev, void *user) { (void)user; seen_total++; if (ev->type == TGB_EVENT_PIXELS_CHANGED) seen_pixels++; }
static tgb_status cmd_seen(tgb_plugin_handle *self, void *user) { (void)self; (void)user; return seen_pixels > 0 ? TGB_OK : TGB_ERR_PLUGIN; }
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
H = host;
tgb_status s = host->subscribe(self, on_event, NULL);
if (s < 0) return s;
host->register_command(self, "eventos-vistos", cmd_seen, NULL);
info->name = "Escucha eventos"; info->version = "1.0"; info->required_caps = TGB_CAP_EVENTS | TGB_CAP_READ_DOCUMENT;
return TGB_OK;
}
+57
View File
@@ -0,0 +1,57 @@
#include "plug_common.h"
TGB_PLUGIN_DECLARE_API_VERSION;
static const tgb_host_api *H;
static const tgb_color RED = {255, 0, 0, 255};
static tgb_status cmd_cross(tgb_plugin_handle *self, void *user) {
(void)user;
tgb_status s = H->edit_begin(self, 0, 0);
if (s < 0) return s;
int cx = H->doc_width(self) / 2, cy = H->doc_height(self) / 2;
for (int i = -3; i <= 3; i++) { H->put_pixel(self, cx + i, cy, RED); H->put_pixel(self, cx, cy + i, RED); }
return H->edit_end(self);
}
static tgb_status cmd_two_steps(tgb_plugin_handle *self, void *user) {
(void)user;
tgb_status s = H->add_frame(self, H->doc_frame_count(self)); /* paso 1: estructura */
if (s < 0) return s;
s = H->edit_begin(self, 0, 0); /* paso 2: píxeles */
if (s < 0) return s;
H->put_pixel(self, 1, 1, RED);
return H->edit_end(self);
}
static tgb_status cmd_fail_midway(tgb_plugin_handle *self, void *user) {
(void)user;
H->add_frame(self, 0);
if (H->edit_begin(self, 0, 0) == TGB_OK) { H->put_pixel(self, 2, 2, RED); H->edit_end(self); }
return TGB_ERR_PLUGIN; /* falla a mitad: el host debe revertirlo todo */
}
static tgb_status cmd_forget_end(tgb_plugin_handle *self, void *user) {
(void)user;
tgb_status s = H->edit_begin(self, 0, 0);
if (s < 0) return s;
H->put_pixel(self, 3, 3, RED);
return TGB_OK; /* olvida edit_end */
}
static tgb_status cmd_read(tgb_plugin_handle *self, void *user) {
(void)user;
return H->get_pixel(self, 0, 0, 0, 0).a == 0 ? TGB_OK : TGB_ERR_PLUGIN;
}
static tgb_status cmd_outside(tgb_plugin_handle *self, void *user) { (void)user; (void)self; return TGB_OK; }
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
H = host;
mark("init");
if (host->api_version != TGB_PLUGIN_API_VERSION || host->struct_size < sizeof(tgb_host_api)) return TGB_ERR_INCOMPATIBLE;
host->register_command(self, "dibujar-cruz", cmd_cross, NULL);
host->register_command(self, "dos-pasos", cmd_two_steps, NULL);
host->register_command(self, "falla-a-medias", cmd_fail_midway, NULL);
host->register_command(self, "olvida-cerrar", cmd_forget_end, NULL);
host->register_command(self, "lee-pixel", cmd_read, NULL);
host->register_command(self, "no-hace-nada", cmd_outside, NULL);
info->name = "Cruz de prueba";
info->version = "1.0.0";
info->required_caps = TGB_CAP_READ_DOCUMENT | TGB_CAP_EDIT_DOCUMENT;
return TGB_OK;
}
TGB_PLUGIN_EXPORT void tgb_plugin_shutdown(tgb_plugin_handle *self) { (void)self; mark("shutdown"); }
+6
View File
@@ -0,0 +1,6 @@
#include "plug_common.h"
TGB_PLUGIN_DECLARE_API_VERSION;
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
(void)host; (void)self; (void)info;
return TGB_ERR_PLUGIN;
}
+9
View File
@@ -0,0 +1,9 @@
#include "plug_common.h"
TGB_PLUGIN_DECLARE_API_VERSION;
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
(void)host; (void)self;
mark("init-needs-edit");
info->name = "Pide editar"; info->version = "1"; info->required_caps = TGB_CAP_EDIT_DOCUMENT;
return TGB_OK;
}
TGB_PLUGIN_EXPORT void tgb_plugin_shutdown(tgb_plugin_handle *self) { (void)self; mark("shutdown-needs-edit"); }
+3
View File
@@ -0,0 +1,3 @@
#include "plug_common.h"
TGB_PLUGIN_DECLARE_API_VERSION;
int plug_unrelated_symbol(void) { return 42; } /* una biblioteca válida pero sin tgb_plugin_init */
+5
View File
@@ -0,0 +1,5 @@
#include "plug_common.h"
TGB_PLUGIN_DECLARE_API_VERSION;
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
(void)host; (void)self; info->name = NULL; info->required_caps = 0; return TGB_OK;
}
+4
View File
@@ -0,0 +1,4 @@
#include "plug_common.h"
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
(void)host; (void)self; (void)info; mark("init-no-version"); return TGB_OK;
}
+5
View File
@@ -0,0 +1,5 @@
#include "plug_common.h"
TGB_PLUGIN_EXPORT const uint32_t tgb_plugin_api_version = 0; /* compilada para otra API */
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
(void)host; (void)self; (void)info; mark("init-old-api"); return TGB_OK;
}
+12
View File
@@ -0,0 +1,12 @@
#include "plug_common.h"
TGB_PLUGIN_DECLARE_API_VERSION;
static const tgb_host_api *H;
static tgb_status cmd_try_edit(tgb_plugin_handle *self, void *user) { (void)user; return H->edit_begin(self, 0, 0); }
static tgb_status cmd_try_edit_unchecked(tgb_plugin_handle *self, void *user) { (void)user; return H->add_frame(self, 0); }
TGB_PLUGIN_EXPORT tgb_status tgb_plugin_init(const tgb_host_api *host, tgb_plugin_handle *self, tgb_plugin_info *info) {
H = host;
host->register_command(self, "intenta-editar", cmd_try_edit, NULL);
host->register_command(self, "intenta-anadir", cmd_try_edit_unchecked, NULL);
info->name = "Solo lectura"; info->version = "0.1"; info->required_caps = TGB_CAP_READ_DOCUMENT; /* NO pide editar */
return TGB_OK;
}
+180
View File
@@ -0,0 +1,180 @@
/* Composición de capas, Onion Skin y reproductor. */
#include <math.h>
#include "tgb_test.h"
static tgb_context *g_ctx;
static char g_root[200];
static tgb_document *new_doc(int w, int h, int fps) {
tgb_document *d = NULL;
CHECK_OK(tgb_document_create(g_ctx, w, h, fps, &d));
return d;
}
TEST(flatten_layers) {
tgb_document *d = new_doc(8, 8, 12);
CHECK_OK(tgb_layer_add(d, 1, "Arriba", NULL));
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
CHECK_OK(paint_px(d, 0, 1, 1, 1, BLUE));
tgb_canvas *out = tgb_canvas_create(8, 8);
CHECK_OK(tgb_render_flatten(d, 0, out));
CHECK(is_px(out, 1, 1, BLUE)); /* la capa de arriba tapa a la de abajo */
CHECK_OK(tgb_layer_set_visible(d, 1, false));
CHECK_OK(tgb_render_flatten(d, 0, out));
CHECK(is_px(out, 1, 1, RED)); /* capa oculta: no cuenta */
CHECK_OK(tgb_layer_set_visible(d, 1, true));
CHECK_OK(tgb_layer_set_opacity(d, 1, 128));
CHECK_OK(tgb_render_flatten(d, 0, out));
tgb_color p = tgb_canvas_get(out, 1, 1);
CHECK(p.a == 255 && abs(p.r - 127) <= 2 && abs(p.b - 128) <= 2); /* mezcla 50 % */
CHECK_OK(tgb_layer_move(d, 1, 0));
CHECK_OK(tgb_layer_set_opacity(d, 0, 255));
CHECK_OK(tgb_render_flatten(d, 0, out));
CHECK(is_px(out, 1, 1, RED)); /* cambiar el orden cambia el resultado */
CHECK_OK(tgb_render_flatten(d, 0, out));
CHECK_EQ(tgb_canvas_dirty(out).w, 0);
tgb_canvas *small = tgb_canvas_create(4, 4);
CHECK_STATUS(tgb_render_flatten(d, 0, small), TGB_ERR_RANGE);
CHECK_STATUS(tgb_render_flatten(d, 5, out), TGB_ERR_RANGE);
CHECK_STATUS(tgb_render_flatten(NULL, 0, out), TGB_ERR_INVALID_ARG);
tgb_canvas_release(small); tgb_canvas_release(out);
tgb_document_destroy(d);
}
TEST(onion_skin) {
tgb_document *d = new_doc(8, 8, 12);
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(tgb_frame_add(d, 2, NULL));
CHECK_OK(paint_px(d, 0, 0, 2, 2, RED)); /* fotograma 0: rojo */
CHECK_OK(paint_px(d, 2, 0, 5, 5, GREEN)); /* fotograma 2: verde */
tgb_onion_settings on; tgb_onion_defaults(&on);
CHECK(!on.enabled);
on.tint = false; on.opacity_before = 0.4f; on.opacity_after = 0.5f; on.falloff = 0.5f;
tgb_canvas *out = tgb_canvas_create(8, 8);
tgb_color none = {0, 0, 0, 0};
/* desactivado: no se ve nada del resto */
CHECK_OK(tgb_render_view(d, 1, &on, none, out));
CHECK(tgb_canvas_is_empty(out));
on.enabled = true;
CHECK_OK(tgb_render_view(d, 1, &on, none, out));
tgb_color pr = tgb_canvas_get(out, 2, 2), pg = tgb_canvas_get(out, 5, 5);
CHECK(pr.r == 255 && pr.g == 0 && abs(pr.a - 102) <= 1); /* anterior: 40 % */
CHECK(pg.g == 255 && abs(pg.a - 128) <= 1); /* posterior: 50 % */
/* tinte: el color original se mezcla con el tinte */
on.tint = true; on.tint_before = BLUE;
CHECK_OK(tgb_render_view(d, 1, &on, none, out));
pr = tgb_canvas_get(out, 2, 2);
CHECK(pr.b > 100 && pr.r < 255);
/* dos fotogramas hacia atrás con atenuación */
on.tint = false; on.frames_before = 2; on.frames_after = 0;
CHECK_OK(tgb_render_view(d, 2, &on, none, out));
CHECK(abs(tgb_canvas_get(out, 2, 2).a - 51) <= 1); /* 0,4 * 0,5 */
CHECK(is_px(out, 5, 5, GREEN)); /* el fotograma actual va encima, sin transparencia */
/* sin fotogramas anteriores al primero */
CHECK_OK(tgb_render_view(d, 0, &on, none, out));
CHECK(is_px(out, 2, 2, RED));
CHECK_EQ(count_alpha(out), 1);
/* fondo opaco */
tgb_color white = {255, 255, 255, 255};
on.frames_after = 2; on.frames_before = 0;
CHECK_OK(tgb_render_view(d, 1, &on, white, out));
CHECK(is_px(out, 0, 0, white));
CHECK(tgb_canvas_get(out, 5, 5).g == 255 && tgb_canvas_get(out, 5, 5).r > 100);
/* validación de ajustes */
tgb_onion_settings bad = on;
bad.frames_before = 99; bad.frames_after = -3; bad.opacity_before = 7.0f; bad.opacity_after = NAN; bad.falloff = -1;
CHECK_OK(tgb_onion_validate(&bad));
CHECK(bad.frames_before == 8 && bad.frames_after == 0 && bad.opacity_before == 1.0f && bad.opacity_after == 0.0f && bad.falloff == 0.0f);
CHECK_STATUS(tgb_onion_validate(NULL), TGB_ERR_INVALID_ARG);
tgb_canvas_release(out);
tgb_document_destroy(d);
}
TEST(player_loop_once_pingpong) {
tgb_document *d = new_doc(8, 8, 10); /* 10 fps = 100 ms por fotograma */
CHECK_OK(tgb_frame_add(d, 1, NULL)); CHECK_OK(tgb_frame_add(d, 2, NULL));
tgb_player *p = tgb_player_create(d);
CHECK(p != NULL);
CHECK_EQ(tgb_player_update(p, 500), 0); /* parado: no avanza */
tgb_player_play(p);
CHECK(tgb_player_is_playing(p));
CHECK_EQ(tgb_player_update(p, 50), 0);
CHECK_EQ(tgb_player_update(p, 60), 1);
CHECK_EQ(tgb_player_update(p, 100), 2);
CHECK_EQ(tgb_player_update(p, 100), 0); /* bucle */
tgb_player_pause(p);
CHECK_EQ(tgb_player_update(p, 1000), 0);
tgb_player_stop(p);
CHECK(!tgb_player_is_playing(p));
/* una vez */
tgb_player_set_mode(p, TGB_PLAY_ONCE);
tgb_player_play(p);
CHECK_EQ(tgb_player_update(p, 100), 1);
CHECK_EQ(tgb_player_update(p, 100), 2);
CHECK_EQ(tgb_player_update(p, 100), 2);
CHECK(!tgb_player_is_playing(p));
tgb_player_play(p); /* volver a dar a play reinicia */
CHECK(tgb_player_is_playing(p) && tgb_player_frame(p) == 0);
/* ping-pong: 0,1,2,1,0,1,2 */
tgb_player_stop(p);
tgb_player_set_mode(p, TGB_PLAY_PINGPONG);
tgb_player_play(p);
size_t expect[] = {1, 2, 1, 0, 1, 2};
for (int i = 0; i < 6; i++) CHECK_EQ(tgb_player_update(p, 100), expect[i]);
/* velocidad doble */
tgb_player_stop(p);
tgb_player_set_mode(p, TGB_PLAY_LOOP);
tgb_player_set_speed(p, 2.0f);
tgb_player_play(p);
CHECK_EQ(tgb_player_update(p, 50), 1);
tgb_player_destroy(p);
CHECK_OK(0);
tgb_document_destroy(d);
}
TEST(player_durations_range_and_edge_cases) {
tgb_document *d = new_doc(8, 8, 10);
CHECK_OK(tgb_frame_add(d, 1, NULL)); CHECK_OK(tgb_frame_add(d, 2, NULL));
CHECK_OK(tgb_frame_set_duration(d, 1, 300)); /* duraciones: 100, 300, 100 */
tgb_player *p = tgb_player_create(d);
tgb_player_play(p);
CHECK_EQ(tgb_player_update(p, 150), 1);
CHECK_EQ(tgb_player_update(p, 240), 1); /* 390 ms: aún en el fotograma de 300 ms (100..400) */
CHECK_EQ(tgb_player_update(p, 20), 2); /* 410 ms: ya en el tercero */
/* un dt gigante no cuelga ni sale del rango */
size_t f = tgb_player_update(p, 1e12);
CHECK(f < 3);
CHECK_EQ(tgb_player_update(p, -5), f); /* dt negativo se ignora */
/* rango 1..2 */
CHECK_STATUS(tgb_player_set_range(p, 2, 1), TGB_ERR_RANGE);
CHECK_STATUS(tgb_player_set_range(p, 0, 9), TGB_ERR_RANGE);
CHECK_OK(tgb_player_set_range(p, 1, 2));
tgb_player_stop(p);
CHECK_EQ(tgb_player_frame(p), 1);
tgb_player_play(p);
CHECK_EQ(tgb_player_update(p, 300), 2);
CHECK_EQ(tgb_player_update(p, 100), 1); /* vuelve al inicio del rango, no al 0 */
tgb_player_clear_range(p);
CHECK_STATUS(tgb_player_seek(p, 7), TGB_ERR_RANGE);
CHECK_OK(tgb_player_seek(p, 2));
CHECK_EQ(tgb_player_frame(p), 2);
/* el documento cambia mientras se reproduce */
CHECK_OK(tgb_frame_delete(d, 2));
CHECK(tgb_player_update(p, 10) < 2);
tgb_player_set_speed(p, -1.0f); tgb_player_set_speed(p, 1000.0f); /* se recortan sin fallar */
tgb_player_destroy(p);
tgb_player_destroy(NULL);
CHECK(tgb_player_create(NULL) == NULL);
tgb_document_destroy(d);
}
int main(void) {
g_ctx = tgb_test_context(g_root, sizeof g_root);
RUN(flatten_layers);
RUN(onion_skin);
RUN(player_loop_once_pingpong);
RUN(player_durations_range_and_edge_cases);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}
+192
View File
@@ -0,0 +1,192 @@
/* Lienzo, herramientas de dibujo, relleno y transformaciones. */
#include <math.h>
#include "tgb_test.h"
TEST(canvas_basics) {
CHECK(tgb_canvas_create(0, 10) == NULL);
CHECK(tgb_canvas_create(10, -1) == NULL);
CHECK(tgb_canvas_create(TGB_MAX_DIMENSION + 1, 10) == NULL);
tgb_canvas *c = tgb_canvas_create(8, 6);
CHECK(c != NULL);
CHECK_EQ(tgb_canvas_width(c), 8);
CHECK_EQ(tgb_canvas_bytes(c), 8 * 6 * 4);
CHECK(tgb_canvas_is_empty(c));
tgb_canvas_set(c, 2, 3, RED);
CHECK(is_px(c, 2, 3, RED));
CHECK(!tgb_canvas_is_empty(c));
CHECK_EQ(tgb_canvas_get(c, -1, 0).a, 0); /* fuera del lienzo = transparente */
CHECK_EQ(tgb_canvas_get(c, 8, 0).a, 0);
tgb_canvas_set(c, 100, 100, RED); /* fuera: se ignora sin fallar */
tgb_canvas *d = tgb_canvas_clone(c);
CHECK(tgb_canvas_equal(c, d));
tgb_canvas_set(d, 0, 0, BLUE);
CHECK(!tgb_canvas_equal(c, d));
CHECK_EQ(tgb_canvas_refs(c), 1);
tgb_canvas_retain(c);
CHECK_EQ(tgb_canvas_refs(c), 2);
tgb_canvas_release(c);
tgb_canvas_release(c);
tgb_canvas_release(d);
tgb_canvas_release(NULL); /* aceptar NULL */
}
TEST(canvas_blend_math) {
tgb_canvas *c = tgb_canvas_create(2, 1);
tgb_color half_red = {255, 0, 0, 128};
tgb_canvas_blend(c, 0, 0, half_red); /* sobre transparente: queda igual */
CHECK(is_px(c, 0, 0, half_red));
tgb_canvas_set(c, 1, 0, BLUE);
tgb_canvas_blend(c, 1, 0, half_red); /* 50 % rojo sobre azul opaco */
tgb_color p = tgb_canvas_get(c, 1, 0);
CHECK_EQ(p.a, 255);
CHECK(abs(p.r - 128) <= 1 && abs(p.b - 127) <= 1 && p.g == 0);
tgb_canvas_erase_px(c, 1, 0, 255); /* borrado total: forma canónica 0,0,0,0 */
CHECK(is_px(c, 1, 0, (tgb_color){0, 0, 0, 0}));
tgb_canvas_release(c);
}
TEST(canvas_crop_blit_dirty) {
tgb_canvas *c = tgb_canvas_create(10, 10);
CHECK_EQ(tgb_canvas_dirty(c).w, 0);
tgb_canvas_set(c, 3, 4, RED);
tgb_canvas_set(c, 6, 8, RED);
tgb_rect d = tgb_canvas_dirty(c);
CHECK(d.x == 3 && d.y == 4 && d.w == 4 && d.h == 5);
tgb_canvas_reset_dirty(c);
CHECK_EQ(tgb_canvas_dirty(c).w, 0);
tgb_canvas *crop = NULL;
CHECK_OK(tgb_canvas_crop(c, (tgb_rect){3, 4, 4, 5}, &crop));
CHECK(is_px(crop, 0, 0, RED) && is_px(crop, 3, 4, RED));
tgb_canvas *bad = NULL;
CHECK_STATUS(tgb_canvas_crop(c, (tgb_rect){8, 8, 5, 5}, &bad), TGB_ERR_RANGE);
tgb_canvas *dst = tgb_canvas_create(10, 10);
tgb_canvas_blit(dst, crop, 1, 1, 128, true); /* mitad de opacidad */
CHECK_EQ(tgb_canvas_get(dst, 1, 1).a, 128);
tgb_canvas_blit(dst, crop, 20, 20, 255, true); /* totalmente fuera: no falla */
tgb_canvas_release(crop); tgb_canvas_release(dst); tgb_canvas_release(c);
}
TEST(pencil_and_brush) {
tgb_canvas *c = tgb_canvas_create(20, 20);
tgb_brush pen = tgb_brush_pencil(RED, 1);
tgb_draw_line(c, &pen, 0, 0, 9, 9);
CHECK_EQ(count_alpha(c), 10); /* diagonal exacta de 10 píxeles */
for (int i = 0; i < 10; i++) CHECK(is_px(c, i, i, RED));
tgb_canvas_clear(c);
tgb_draw_line(c, &pen, 2, 5, 12, 5); /* horizontal */
CHECK_EQ(count_alpha(c), 11);
tgb_canvas_clear(c);
tgb_brush fat = tgb_brush_pencil(BLUE, 3); /* lápiz grueso sin suavizado: todo opaco */
tgb_draw_dab(c, &fat, 10.5f, 10.5f);
bool all_opaque = true;
for (int y = 0; y < 20; y++) for (int x = 0; x < 20; x++) { tgb_color p = tgb_canvas_get(c, x, y); if (p.a && p.a != 255) all_opaque = false; }
CHECK(all_opaque);
CHECK(count_alpha(c) >= 5 && count_alpha(c) <= 9);
tgb_canvas_clear(c);
tgb_brush soft = tgb_brush_soft(BLACK, 10.0f, 0.5f);
tgb_draw_dab(c, &soft, 10.0f, 10.0f);
CHECK_EQ(tgb_canvas_get(c, 10, 10).a, 255); /* centro opaco */
int a = tgb_canvas_get(c, 14, 10).a;
CHECK(a > 0 && a < 255); /* borde suave */
CHECK_EQ(tgb_canvas_get(c, 17, 10).a, 0);
tgb_brush er = tgb_brush_eraser(6.0f, 1.0f);
tgb_draw_dab(c, &er, 10.0f, 10.0f);
CHECK_EQ(tgb_canvas_get(c, 10, 10).a, 0); /* el borrador quita opacidad */
tgb_canvas_release(c);
}
TEST(pencil_robust_inputs) {
tgb_canvas *c = tgb_canvas_create(16, 16);
tgb_brush pen = tgb_brush_pencil(RED, 1);
tgb_draw_line(c, &pen, NAN, 0, 5, 5); /* no debe colgarse ni escribir fuera */
tgb_draw_line(c, &pen, -1e30f, -1e30f, 1e30f, 1e30f);
tgb_draw_dab(c, &pen, INFINITY, 3);
tgb_draw_line(c, NULL, 0, 0, 1, 1);
tgb_draw_dab(NULL, &pen, 0, 0);
CHECK(c != NULL);
tgb_canvas_release(c);
}
TEST(shapes) {
tgb_canvas *c = tgb_canvas_create(30, 30);
tgb_brush pen = tgb_brush_pencil(RED, 1);
tgb_draw_rect(c, &pen, (tgb_rect){2, 2, 4, 3}, true);
CHECK_EQ(count_alpha(c), 12);
tgb_canvas_clear(c);
tgb_draw_rect(c, &pen, (tgb_rect){0, 0, 5, 5}, false);
CHECK_EQ(count_alpha(c), 16); /* perímetro de 5x5 */
CHECK(!is_px(c, 2, 2, RED));
tgb_canvas_clear(c);
tgb_draw_ellipse(c, &pen, (tgb_rect){5, 5, 9, 9}, true);
CHECK(is_px(c, 9, 9, RED));
CHECK(!is_px(c, 5, 5, RED));
bool sym = true; /* simetría horizontal */
for (int y = 5; y < 14; y++) for (int x = 5; x < 14; x++) if (is_px(c, x, y, RED) != is_px(c, 18 - x, y, RED)) sym = false;
CHECK(sym);
tgb_canvas_clear(c);
tgb_draw_ellipse(c, &pen, (tgb_rect){5, 5, 15, 9}, false);
CHECK(count_alpha(c) > 20);
CHECK(!is_px(c, 12, 9, RED)); /* hueco en el centro */
tgb_canvas_clear(c);
tgb_draw_bezier(c, &pen, 2, 2, 10, 25, 20, 25, 27, 2);
CHECK(is_px(c, 2, 2, RED) && is_px(c, 27, 2, RED));
CHECK(count_alpha(c) > 25);
tgb_canvas_release(c);
}
TEST(flood_fill_and_eyedropper) {
tgb_canvas *c = tgb_canvas_create(10, 10);
for (int y = 0; y < 10; y++) tgb_canvas_set(c, 5, y, BLACK); /* muro vertical */
size_t n = tgb_flood_fill(c, 1, 1, RED, 0);
CHECK_EQ(n, 50);
CHECK(is_px(c, 0, 9, RED) && is_px(c, 4, 0, RED));
CHECK(is_px(c, 5, 5, BLACK));
CHECK_EQ(tgb_canvas_get(c, 7, 7).a, 0); /* el otro lado no se toca */
CHECK_EQ(tgb_flood_fill(c, 1, 1, RED, 0), 0); /* mismo color: no hace nada (y no se cuelga) */
CHECK_EQ(tgb_flood_fill(c, -1, 0, RED, 0), 0);
tgb_color e = tgb_eyedropper(c, 5, 3);
CHECK(e.r == 0 && e.a == 255);
/* tolerancia: gris casi igual se rellena con tolerancia alta */
tgb_canvas *g = tgb_canvas_create(4, 1);
for (int x = 0; x < 4; x++) { tgb_color p = {(uint8_t)(100 + x * 3), 100, 100, 255}; tgb_canvas_set(g, x, 0, p); }
CHECK_EQ(tgb_flood_fill(g, 0, 0, GREEN, 0), 1);
tgb_canvas_set(g, 0, 0, (tgb_color){100, 100, 100, 255});
CHECK_EQ(tgb_flood_fill(g, 0, 0, GREEN, 10), 4);
tgb_canvas_release(g); tgb_canvas_release(c);
}
TEST(regions) {
tgb_canvas *c = tgb_canvas_create(10, 10);
tgb_canvas_fill_rect(c, (tgb_rect){1, 1, 2, 2}, RED, false);
CHECK_OK(tgb_region_move(c, (tgb_rect){0, 0, 4, 4}, 3, 2));
CHECK(is_px(c, 4, 3, RED) && is_px(c, 5, 4, RED));
CHECK_EQ(tgb_canvas_get(c, 1, 1).a, 0);
CHECK_EQ(count_alpha(c), 4);
CHECK_STATUS(tgb_region_move(c, (tgb_rect){50, 50, 2, 2}, 1, 1), TGB_ERR_RANGE);
tgb_canvas_clear(c);
tgb_canvas_set(c, 1, 1, RED);
CHECK_OK(tgb_region_flip(c, (tgb_rect){0, 0, 4, 4}, true));
CHECK(is_px(c, 2, 1, RED) && count_alpha(c) == 1);
CHECK_OK(tgb_region_flip(c, (tgb_rect){0, 0, 4, 4}, false));
CHECK(is_px(c, 2, 2, RED) && count_alpha(c) == 1);
tgb_canvas_clear(c);
tgb_canvas_fill_rect(c, (tgb_rect){0, 0, 2, 2}, BLUE, false);
CHECK_OK(tgb_region_scale(c, (tgb_rect){0, 0, 2, 2}, 4, 4));
CHECK_EQ(count_alpha(c), 16);
CHECK_STATUS(tgb_region_scale(c, (tgb_rect){0, 0, 2, 2}, 0, 4), TGB_ERR_RANGE);
CHECK_OK(tgb_region_clear(c, (tgb_rect){0, 0, 10, 10}));
CHECK(tgb_canvas_is_empty(c));
tgb_canvas_release(c);
}
int main(void) {
RUN(canvas_basics);
RUN(canvas_blend_math);
RUN(canvas_crop_blit_dirty);
RUN(pencil_and_brush);
RUN(pencil_robust_inputs);
RUN(shapes);
RUN(flood_fill_and_eyedropper);
RUN(regions);
TEST_SUMMARY();
}
+487
View File
@@ -0,0 +1,487 @@
/* Modelo de documento, sesiones de edición e historial. */
#include <dirent.h>
#include "tgb_test.h"
static tgb_context *g_ctx;
static char g_root[200];
static tgb_document *new_doc(int w, int h, int fps) {
tgb_document *d = NULL;
CHECK_OK(tgb_document_create(g_ctx, w, h, fps, &d));
return d;
}
static tgb_status paint_block(tgb_document *doc, size_t f, size_t l, int x, int y, int size, tgb_color col) {
tgb_status s = tgb_edit_begin(doc, tgb_frame_id(doc, f), tgb_layer_id(doc, l), "Bloque");
if (s != TGB_OK) return s;
tgb_canvas_fill_rect(tgb_edit_canvas(doc), (tgb_rect){x, y, size, size}, col, false);
return tgb_edit_end(doc);
}
static size_t undo_count(const tgb_document *d) { tgb_history_stats s; tgb_history_get_stats(d, &s); return s.undo_count; }
TEST(document_create_and_props) {
tgb_document *d = NULL;
CHECK_STATUS(tgb_document_create(NULL, 10, 10, 12, &d), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_document_create(g_ctx, 0, 10, 12, &d), TGB_ERR_RANGE);
CHECK_STATUS(tgb_document_create(g_ctx, 10, 10, 0, &d), TGB_ERR_RANGE);
CHECK_STATUS(tgb_document_create(g_ctx, 10, 10, 61, &d), TGB_ERR_RANGE);
CHECK(d == NULL);
d = new_doc(64, 48, 12);
CHECK_EQ(tgb_document_width(d), 64);
CHECK_EQ(tgb_document_height(d), 48);
CHECK_EQ(tgb_document_fps(d), 12);
CHECK_EQ(tgb_frame_count(d), 1);
CHECK_EQ(tgb_layer_count(d), 1);
CHECK(!strcmp(tgb_layer_name(d, 0), "Capa 1"));
CHECK_EQ(tgb_frame_duration_ms(d, 0), 83);
CHECK(tgb_frame_cel(d, 0, 0) == NULL); /* celda vacía: no gasta memoria */
CHECK(!tgb_document_dirty(d));
CHECK(tgb_document_is_consistent(d));
CHECK(tgb_document_path(d) == NULL);
CHECK_OK(tgb_document_set_path(d, "/tmp/x.tgb"));
CHECK(!strcmp(tgb_document_path(d), "/tmp/x.tgb"));
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
CHECK(tgb_document_dirty(d));
tgb_document_mark_saved(d);
CHECK(!tgb_document_dirty(d));
CHECK_OK(tgb_history_undo(d));
CHECK(tgb_document_dirty(d)); /* deshacer también cambia el documento */
tgb_document_destroy(d);
tgb_document_destroy(NULL);
}
TEST(edit_session_undo_redo) {
tgb_document *d = new_doc(32, 32, 12);
CHECK(!tgb_history_can_undo(d));
CHECK_STATUS(tgb_history_undo(d), TGB_ERR_NOTHING);
CHECK_STATUS(tgb_history_redo(d), TGB_ERR_NOTHING);
CHECK_OK(paint_px(d, 0, 0, 3, 4, RED));
CHECK(is_px(tgb_frame_cel(d, 0, 0), 3, 4, RED));
CHECK(tgb_history_can_undo(d));
CHECK(!strcmp(tgb_history_undo_label(d), "Punto"));
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(px_at(d, 0, 0, 3, 4).a, 0);
CHECK(tgb_history_can_redo(d));
CHECK(!strcmp(tgb_history_redo_label(d), "Punto"));
CHECK_OK(tgb_history_redo(d));
CHECK(is_px(tgb_frame_cel(d, 0, 0), 3, 4, RED));
/* una sesión sin cambios reales no crea operación */
size_t before = undo_count(d);
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "Nada"));
tgb_canvas_set(tgb_edit_canvas(d), 3, 4, RED); /* mismo valor */
CHECK_OK(tgb_edit_end(d));
CHECK_EQ(undo_count(d), before);
/* estados inválidos */
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "A"));
CHECK(tgb_edit_active(d));
CHECK_STATUS(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "B"), TGB_ERR_STATE);
CHECK_STATUS(tgb_history_undo(d), TGB_ERR_STATE);
CHECK_STATUS(tgb_frame_add(d, 1, NULL), TGB_ERR_STATE);
CHECK_STATUS(tgb_layer_add(d, 1, "x", NULL), TGB_ERR_STATE);
/* cancelar devuelve la celda a como estaba */
tgb_canvas_set(tgb_edit_canvas(d), 10, 10, BLUE);
CHECK_OK(tgb_edit_cancel(d));
CHECK_EQ(px_at(d, 0, 0, 10, 10).a, 0);
CHECK(!tgb_edit_active(d));
CHECK_STATUS(tgb_edit_end(d), TGB_ERR_STATE);
CHECK_STATUS(tgb_edit_cancel(d), TGB_ERR_STATE);
CHECK(tgb_edit_canvas(d) == NULL);
CHECK_STATUS(tgb_edit_begin(d, 9999, tgb_layer_id(d, 0), "x"), TGB_ERR_NOT_FOUND);
/* reset = volver al inicio de la sesión (vista previa de formas) */
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "Línea"));
tgb_brush pen = tgb_brush_pencil(GREEN, 1);
tgb_draw_line(tgb_edit_canvas(d), &pen, 0, 20, 10, 20);
CHECK_OK(tgb_edit_reset(d));
tgb_draw_line(tgb_edit_canvas(d), &pen, 0, 25, 10, 25);
CHECK_OK(tgb_edit_end(d));
CHECK_EQ(px_at(d, 0, 0, 5, 20).a, 0);
CHECK(is_px(tgb_frame_cel(d, 0, 0), 5, 25, GREEN));
tgb_document_destroy(d);
}
TEST(locked_layer_rejects_edits) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_OK(tgb_layer_set_locked(d, 0, true));
CHECK_STATUS(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "x"), TGB_ERR_LOCKED);
CHECK(!tgb_edit_active(d));
CHECK_OK(tgb_history_undo(d)); /* desbloquear por deshacer */
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
tgb_document_destroy(d);
}
TEST(multiple_strokes_and_redo_cleared) {
tgb_document *d = new_doc(16, 16, 12);
for (int i = 0; i < 3; i++) CHECK_OK(paint_px(d, 0, 0, i, 0, RED));
CHECK_EQ(undo_count(d), 3);
CHECK_OK(tgb_history_undo(d)); CHECK_OK(tgb_history_undo(d));
CHECK(is_px(tgb_frame_cel(d, 0, 0), 0, 0, RED));
CHECK_EQ(px_at(d, 0, 0, 1, 0).a, 0);
CHECK(tgb_history_can_redo(d));
CHECK_OK(paint_px(d, 0, 0, 9, 9, BLUE)); /* una acción nueva invalida el "rehacer" */
CHECK(!tgb_history_can_redo(d));
CHECK_STATUS(tgb_history_redo(d), TGB_ERR_NOTHING);
tgb_document_destroy(d);
}
TEST(frame_operations) {
tgb_document *d = new_doc(16, 16, 10);
uint32_t id1 = tgb_frame_id(d, 0), id2 = 0, id3 = 0;
CHECK_OK(tgb_frame_add(d, 1, &id2));
CHECK_OK(tgb_frame_add(d, 0, &id3));
CHECK_EQ(tgb_frame_count(d), 3);
CHECK(tgb_frame_id(d, 0) == id3 && tgb_frame_id(d, 1) == id1 && tgb_frame_id(d, 2) == id2);
size_t idx = 99;
CHECK_OK(tgb_frame_index_of(d, id2, &idx));
CHECK_EQ(idx, 2);
CHECK_STATUS(tgb_frame_index_of(d, 424242, &idx), TGB_ERR_NOT_FOUND);
CHECK_STATUS(tgb_frame_add(d, 9, NULL), TGB_ERR_RANGE);
CHECK_OK(tgb_frame_move(d, 0, 2));
CHECK(tgb_frame_id(d, 2) == id3 && tgb_frame_id(d, 0) == id1);
CHECK_STATUS(tgb_frame_move(d, 0, 7), TGB_ERR_RANGE);
CHECK_OK(tgb_history_undo(d)); /* deshacer el movimiento */
CHECK(tgb_frame_id(d, 0) == id3 && tgb_frame_id(d, 2) == id2);
CHECK_OK(tgb_frame_delete(d, 1));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(tgb_frame_count(d), 3);
CHECK(tgb_frame_id(d, 1) == id1);
CHECK_OK(tgb_history_redo(d));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK_OK(tgb_frame_delete(d, 0));
CHECK_STATUS(tgb_frame_delete(d, 0), TGB_ERR_STATE); /* nunca el último */
/* duración */
CHECK_STATUS(tgb_frame_set_duration(d, 0, 5), TGB_ERR_RANGE);
CHECK_STATUS(tgb_frame_set_duration(d, 0, 70000), TGB_ERR_RANGE);
CHECK_STATUS(tgb_frame_set_duration(d, 5, 100), TGB_ERR_RANGE);
CHECK_OK(tgb_frame_set_duration(d, 0, 250));
CHECK_EQ(tgb_frame_duration_ms(d, 0), 250);
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(tgb_frame_duration_ms(d, 0), 100);
tgb_document_destroy(d);
}
TEST(duplicate_shares_memory_until_edited) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_OK(paint_px(d, 0, 0, 2, 2, RED));
CHECK_OK(tgb_frame_duplicate(d, 0, NULL));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK(tgb_frame_cel(d, 0, 0) == tgb_frame_cel(d, 1, 0)); /* misma celda en memoria */
CHECK_EQ(tgb_canvas_refs(tgb_frame_cel(d, 0, 0)), 2);
CHECK_OK(paint_px(d, 1, 0, 5, 5, BLUE)); /* copy-on-write */
CHECK(tgb_frame_cel(d, 0, 0) != tgb_frame_cel(d, 1, 0));
CHECK_EQ(px_at(d, 0, 0, 5, 5).a, 0); /* el original NO cambia */
CHECK(is_px(tgb_frame_cel(d, 1, 0), 2, 2, RED) && is_px(tgb_frame_cel(d, 1, 0), 5, 5, BLUE));
CHECK_OK(tgb_history_undo(d)); /* deshacer la edición de la copia */
CHECK_EQ(px_at(d, 1, 0, 5, 5).a, 0);
CHECK_OK(tgb_history_undo(d)); /* deshacer la duplicación */
CHECK_EQ(tgb_frame_count(d), 1);
CHECK(is_px(tgb_frame_cel(d, 0, 0), 2, 2, RED));
CHECK_OK(tgb_history_redo(d)); CHECK_OK(tgb_history_redo(d));
CHECK(is_px(tgb_frame_cel(d, 1, 0), 5, 5, BLUE));
CHECK_EQ(px_at(d, 0, 0, 5, 5).a, 0);
tgb_document_destroy(d);
}
TEST(delete_frame_keeps_pixels_for_undo) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(paint_px(d, 1, 0, 7, 7, GREEN));
CHECK_OK(tgb_frame_delete(d, 1));
CHECK_EQ(tgb_frame_count(d), 1);
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK(is_px(tgb_frame_cel(d, 1, 0), 7, 7, GREEN));
CHECK_OK(tgb_history_undo(d)); /* deshacer el dibujo en el fotograma restaurado */
CHECK_EQ(px_at(d, 1, 0, 7, 7).a, 0);
tgb_document_destroy(d);
}
TEST(layer_operations) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_OK(tgb_frame_add(d, 1, NULL));
uint32_t lid = 0;
CHECK_OK(tgb_layer_add(d, 1, "Tinta", &lid));
CHECK_EQ(tgb_layer_count(d), 2);
CHECK(!strcmp(tgb_layer_name(d, 1), "Tinta"));
CHECK(tgb_layer_visible(d, 1) && !tgb_layer_locked(d, 1) && tgb_layer_opacity(d, 1) == 255);
CHECK_STATUS(tgb_layer_add(d, 1, "", NULL), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_layer_add(d, 1, "\xff\xfe", NULL), TGB_ERR_INVALID_ARG); /* UTF-8 inválido */
char longname[200]; memset(longname, 'a', sizeof longname - 1); longname[sizeof longname - 1] = 0;
CHECK_STATUS(tgb_layer_add(d, 1, longname, NULL), TGB_ERR_LIMIT);
CHECK_STATUS(tgb_layer_add(d, 9, "x", NULL), TGB_ERR_RANGE);
CHECK_OK(paint_px(d, 0, 1, 4, 4, RED));
CHECK_OK(paint_px(d, 1, 1, 6, 6, BLUE));
CHECK_OK(tgb_layer_move(d, 1, 0));
CHECK(tgb_layer_id(d, 0) == lid);
CHECK(is_px(tgb_frame_cel(d, 0, 0), 4, 4, RED)); /* las celdas se mueven con su capa */
CHECK_OK(tgb_history_undo(d));
CHECK(tgb_layer_id(d, 1) == lid && is_px(tgb_frame_cel(d, 0, 1), 4, 4, RED));
CHECK_OK(tgb_layer_rename(d, 1, "Color"));
CHECK(!strcmp(tgb_layer_name(d, 1), "Color"));
CHECK_OK(tgb_layer_set_visible(d, 1, false));
CHECK_OK(tgb_layer_set_opacity(d, 1, 128));
CHECK(!tgb_layer_visible(d, 1) && tgb_layer_opacity(d, 1) == 128);
CHECK_OK(tgb_history_undo(d)); CHECK_OK(tgb_history_undo(d)); CHECK_OK(tgb_history_undo(d));
CHECK(!strcmp(tgb_layer_name(d, 1), "Tinta") && tgb_layer_visible(d, 1) && tgb_layer_opacity(d, 1) == 255);
CHECK_OK(tgb_history_redo(d));
CHECK(!strcmp(tgb_layer_name(d, 1), "Color"));
/* borrar capa con dibujos y deshacer */
CHECK_OK(tgb_layer_delete(d, 1));
CHECK_EQ(tgb_layer_count(d), 1);
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(tgb_layer_count(d), 2);
CHECK(is_px(tgb_frame_cel(d, 0, 1), 4, 4, RED) && is_px(tgb_frame_cel(d, 1, 1), 6, 6, BLUE));
CHECK_OK(tgb_layer_delete(d, 1));
CHECK_STATUS(tgb_layer_delete(d, 0), TGB_ERR_STATE); /* nunca la última */
tgb_document_destroy(d);
}
TEST(fps_change_is_one_undo_step) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_OK(tgb_frame_add(d, 1, NULL));
size_t before = undo_count(d);
CHECK_OK(tgb_document_set_fps(d, 24, true));
CHECK_EQ(tgb_document_fps(d), 24);
CHECK_EQ(tgb_frame_duration_ms(d, 0), 42);
CHECK_EQ(tgb_frame_duration_ms(d, 1), 42);
CHECK_EQ(undo_count(d), before + 1);
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(tgb_document_fps(d), 12);
CHECK_EQ(tgb_frame_duration_ms(d, 1), 83);
CHECK_STATUS(tgb_document_set_fps(d, 0, true), TGB_ERR_RANGE);
tgb_document_destroy(d);
}
TEST(frame_clipboard) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_STATUS(tgb_frame_paste(d, 0, NULL), TGB_ERR_STATE); /* portapapeles vacío */
CHECK_OK(paint_px(d, 0, 0, 2, 3, RED));
CHECK_OK(tgb_frame_copy(d, 0));
uint32_t id = 0;
CHECK_OK(tgb_frame_paste(d, 1, &id));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK(tgb_frame_id(d, 1) == id);
CHECK(is_px(tgb_frame_cel(d, 1, 0), 2, 3, RED));
tgb_document *other = new_doc(20, 20, 12); /* otras dimensiones */
CHECK_STATUS(tgb_frame_paste(other, 0, NULL), TGB_ERR_UNSUPPORTED);
CHECK_STATUS(tgb_frame_copy(d, 9), TGB_ERR_RANGE);
tgb_document_destroy(other);
tgb_document_destroy(d);
}
TEST(groups) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_STATUS(tgb_history_end_group(d), TGB_ERR_STATE);
CHECK_OK(tgb_history_begin_group(d, "Tres puntos"));
for (int i = 0; i < 3; i++) CHECK_OK(paint_px(d, 0, 0, i, 0, RED));
CHECK(!tgb_history_can_undo(d)); /* con un grupo abierto no se deshace */
CHECK_STATUS(tgb_history_undo(d), TGB_ERR_STATE);
CHECK_OK(tgb_history_end_group(d));
CHECK_EQ(undo_count(d), 1);
CHECK(!strcmp(tgb_history_undo_label(d), "Tres puntos"));
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(count_alpha(tgb_frame_cel(d, 0, 0)), 0);
CHECK_OK(tgb_history_redo(d));
CHECK_EQ(count_alpha(tgb_frame_cel(d, 0, 0)), 3);
/* anidados y mezclando estructura + píxeles */
CHECK_OK(tgb_history_begin_group(d, "Exterior"));
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(tgb_history_begin_group(d, "Interior"));
CHECK_OK(paint_px(d, 1, 0, 5, 5, BLUE));
CHECK_OK(tgb_layer_add(d, 1, "Extra", NULL));
CHECK_OK(tgb_history_end_group(d));
CHECK_OK(paint_px(d, 0, 1, 1, 1, GREEN));
CHECK_OK(tgb_history_end_group(d));
CHECK_EQ(undo_count(d), 2);
CHECK_EQ(tgb_frame_count(d), 2);
CHECK_EQ(tgb_layer_count(d), 2);
CHECK_OK(tgb_history_undo(d)); /* todo el grupo exterior de una vez */
CHECK_EQ(tgb_frame_count(d), 1);
CHECK_EQ(tgb_layer_count(d), 1);
CHECK_OK(tgb_history_redo(d));
CHECK(is_px(tgb_frame_cel(d, 1, 0), 5, 5, BLUE) && is_px(tgb_frame_cel(d, 0, 1), 1, 1, GREEN));
/* grupo vacío: no crea operación */
size_t n = undo_count(d);
CHECK_OK(tgb_history_begin_group(d, "Vacío"));
CHECK_OK(tgb_history_end_group(d));
CHECK_EQ(undo_count(d), n);
/* sesión de edición abierta impide cerrar el grupo */
CHECK_OK(tgb_history_begin_group(d, "G"));
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "x"));
CHECK_STATUS(tgb_history_end_group(d), TGB_ERR_STATE);
CHECK_OK(tgb_edit_cancel(d));
CHECK_OK(tgb_history_end_group(d));
tgb_document_destroy(d);
}
TEST(group_abort_restores_state) {
tgb_document *d = new_doc(16, 16, 12);
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
size_t n = undo_count(d);
CHECK_OK(tgb_history_begin_group(d, "Fallido"));
CHECK_OK(paint_px(d, 0, 0, 2, 2, BLUE));
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(tgb_layer_add(d, 1, "L", NULL));
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "pendiente")); /* sesión abierta a medias */
tgb_canvas_set(tgb_edit_canvas(d), 8, 8, GREEN);
CHECK_OK(tgb_history_abort_group(d));
CHECK(!tgb_edit_active(d));
CHECK_EQ(tgb_frame_count(d), 1);
CHECK_EQ(tgb_layer_count(d), 1);
CHECK_EQ(count_alpha(tgb_frame_cel(d, 0, 0)), 1);
CHECK(is_px(tgb_frame_cel(d, 0, 0), 1, 1, RED));
CHECK_EQ(undo_count(d), n); /* el grupo abortado no queda en el historial */
CHECK(tgb_document_is_consistent(d));
CHECK_STATUS(tgb_history_abort_group(d), TGB_ERR_STATE);
tgb_document_destroy(d);
}
TEST(limit_operation_count) {
tgb_document *d = new_doc(16, 16, 12);
tgb_history_limits lim; tgb_history_get_limits(d, &lim);
lim.max_operations = 3;
CHECK_OK(tgb_history_set_limits(d, &lim));
for (int i = 0; i < 5; i++) CHECK_OK(paint_px(d, 0, 0, i, 0, RED));
tgb_history_stats st; tgb_history_get_stats(d, &st);
CHECK_EQ(st.undo_count, 3);
CHECK_EQ(st.dropped_ops, 2);
CHECK_OK(tgb_history_undo(d)); CHECK_OK(tgb_history_undo(d)); CHECK_OK(tgb_history_undo(d));
CHECK_STATUS(tgb_history_undo(d), TGB_ERR_NOTHING);
CHECK_EQ(count_alpha(tgb_frame_cel(d, 0, 0)), 2); /* lo más antiguo ya no se puede deshacer */
lim.max_operations = 0;
CHECK_STATUS(tgb_history_set_limits(d, &lim), TGB_ERR_RANGE);
tgb_document_destroy(d);
}
TEST(limit_memory_drops_oldest_without_disk) {
tgb_document *d = new_doc(256, 64, 12);
tgb_history_limits lim; tgb_history_get_limits(d, &lim);
lim.max_memory_bytes = 12000; lim.max_temp_bytes = 0; lim.temp_dir[0] = '\0';
CHECK_OK(tgb_history_set_limits(d, &lim));
for (int i = 0; i < 10; i++) CHECK_OK(paint_block(d, 0, 0, i * 24, 0, 20, RED)); /* ~3,3 KB cada una */
tgb_history_stats st; tgb_history_get_stats(d, &st);
CHECK(st.memory_bytes <= 12000);
CHECK(st.dropped_ops > 0 && st.undo_count + st.dropped_ops == 10);
size_t undone = 0;
while (tgb_history_undo(d) == TGB_OK) undone++;
CHECK_EQ(undone, st.undo_count);
/* una sola operación mayor que todo el presupuesto: se aplica, pero avisa de que no se puede deshacer */
lim.max_memory_bytes = 100;
CHECK_OK(tgb_history_set_limits(d, &lim));
CHECK_STATUS(paint_block(d, 0, 0, 0, 30, 20, BLUE), TGB_WARN_HISTORY_LOST);
CHECK(is_px(tgb_frame_cel(d, 0, 0), 5, 35, BLUE)); /* la edición SÍ se aplicó */
CHECK(!tgb_history_can_undo(d));
tgb_document_destroy(d);
}
static size_t count_spill_files(const char *dir) {
DIR *dp = opendir(dir);
if (!dp) return 0;
size_t n = 0; struct dirent *e;
while ((e = readdir(dp))) if (!strncmp(e->d_name, "tgb-hist-", 9)) n++;
closedir(dp);
return n;
}
TEST(limit_memory_spills_to_disk_and_restores) {
tgb_document *d = new_doc(256, 64, 12);
char dir[500]; snprintf(dir, sizeof dir, "%s/spill", g_root);
tgb_history_limits lim; tgb_history_get_limits(d, &lim);
lim.max_memory_bytes = 10000;
lim.max_temp_bytes = 5u * 1024 * 1024;
snprintf(lim.temp_dir, sizeof lim.temp_dir, "%s", dir);
CHECK_OK(tgb_history_set_limits(d, &lim));
for (int i = 0; i < 10; i++) CHECK_OK(paint_block(d, 0, 0, i * 24, 0, 20, RED));
tgb_history_stats st; tgb_history_get_stats(d, &st);
CHECK(st.spilled_ops > 0);
CHECK(st.temp_bytes > 0);
CHECK(st.memory_bytes <= 10000);
CHECK_EQ(st.dropped_ops, 0); /* con disco disponible no se pierde nada */
CHECK_EQ(st.undo_count, 10);
CHECK(count_spill_files(dir) > 0);
for (int i = 9; i >= 0; i--) { /* deshacer TODO, incluidas las operaciones en disco */
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(px_at(d, 0, 0, i * 24, 0).a, 0);
if (i > 0) CHECK(is_px(tgb_frame_cel(d, 0, 0), (i - 1) * 24, 0, RED));
}
for (int i = 0; i < 10; i++) CHECK_OK(tgb_history_redo(d));
CHECK_EQ(count_alpha(tgb_frame_cel(d, 0, 0)), 4000);
tgb_document_destroy(d);
CHECK_EQ(count_spill_files(dir), 0); /* no quedan archivos temporales */
}
TEST(limit_disk_exhausted_drops_oldest) {
tgb_document *d = new_doc(256, 64, 12);
char dir[500]; snprintf(dir, sizeof dir, "%s/spill2", g_root);
tgb_history_limits lim; tgb_history_get_limits(d, &lim);
lim.max_memory_bytes = 6000; lim.max_temp_bytes = 8000;
snprintf(lim.temp_dir, sizeof lim.temp_dir, "%s", dir);
CHECK_OK(tgb_history_set_limits(d, &lim));
for (int i = 0; i < 12; i++) CHECK_OK(paint_block(d, 0, 0, i * 17, 0, 16, RED));
tgb_history_stats st; tgb_history_get_stats(d, &st);
CHECK(st.memory_bytes <= 6000 && st.temp_bytes <= 8000);
CHECK(st.dropped_ops > 0);
CHECK(tgb_document_is_consistent(d));
tgb_document_destroy(d);
}
typedef struct { int counts[16]; } event_counts;
static void on_event(const tgb_event *ev, void *user) { ((event_counts *)user)->counts[ev->type]++; }
TEST(events_are_emitted) {
tgb_document *d = new_doc(16, 16, 12);
event_counts ec; memset(&ec, 0, sizeof ec);
uint32_t sub = 0;
CHECK_OK(tgb_events_subscribe(g_ctx, on_event, &ec, &sub));
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
CHECK(ec.counts[TGB_EVENT_PIXELS_CHANGED] >= 1 && ec.counts[TGB_EVENT_HISTORY_CHANGED] >= 1 && ec.counts[TGB_EVENT_DOCUMENT_CHANGED] >= 1);
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK(ec.counts[TGB_EVENT_FRAMES_CHANGED] >= 1);
CHECK_OK(tgb_layer_add(d, 1, "L", NULL));
CHECK(ec.counts[TGB_EVENT_LAYERS_CHANGED] >= 1);
CHECK_OK(tgb_events_unsubscribe(g_ctx, sub));
int before = ec.counts[TGB_EVENT_PIXELS_CHANGED];
CHECK_OK(paint_px(d, 0, 0, 2, 2, RED));
CHECK_EQ(ec.counts[TGB_EVENT_PIXELS_CHANGED], before); /* ya no recibe */
CHECK_STATUS(tgb_events_unsubscribe(g_ctx, sub), TGB_ERR_NOT_FOUND);
tgb_document_destroy(d);
}
TEST(adaptive_limits) {
tgb_document *d = new_doc(960, 540, 12);
tgb_history_limits lim;
tgb_history_get_limits(d, &lim);
CHECK(lim.max_memory_bytes >= 960u * 540u * 4u * 8u); /* al menos 8 celdas completas */
CHECK(lim.max_operations >= 100);
CHECK_OK(tgb_history_adapt(d));
tgb_history_get_limits(d, &lim);
CHECK(lim.max_memory_bytes > 0);
tgb_document_destroy(d);
}
int main(void) {
g_ctx = tgb_test_context(g_root, sizeof g_root);
RUN(document_create_and_props);
RUN(edit_session_undo_redo);
RUN(locked_layer_rejects_edits);
RUN(multiple_strokes_and_redo_cleared);
RUN(frame_operations);
RUN(duplicate_shares_memory_until_edited);
RUN(delete_frame_keeps_pixels_for_undo);
RUN(layer_operations);
RUN(fps_change_is_one_undo_step);
RUN(frame_clipboard);
RUN(groups);
RUN(group_abort_restores_state);
RUN(limit_operation_count);
RUN(limit_memory_drops_oldest_without_disk);
RUN(limit_memory_spills_to_disk_and_restores);
RUN(limit_disk_exhausted_drops_oldest);
RUN(events_are_emitted);
RUN(adaptive_limits);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}
+323
View File
@@ -0,0 +1,323 @@
/* Guardado atómico, importación y exportación de archivos reales. */
#include <dirent.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <zlib.h>
#include "tgb_test.h"
#include "fixtures_png.h"
static tgb_context *g_ctx;
static char g_root[200];
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 void path_in(char *out, size_t cap, const char *name) { snprintf(out, cap, "%s/%s", g_root, name); }
static uint8_t *slurp(const char *path, size_t *len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
uint8_t *b = malloc((size_t)n + 1);
size_t got = fread(b, 1, (size_t)n, f);
fclose(f);
*len = got;
return b;
}
static void spit(const char *path, const void *data, size_t len) {
FILE *f = fopen(path, "wb");
fwrite(data, 1, len, f);
fclose(f);
}
static size_t count_tmp_files(void) {
DIR *dp = opendir(g_root);
size_t n = 0; struct dirent *e;
while (dp && (e = readdir(dp))) if (strstr(e->d_name, ".tmp-")) n++;
if (dp) closedir(dp);
return n;
}
static size_t undo_count(const tgb_document *d) { tgb_history_stats s; tgb_history_get_stats(d, &s); return s.undo_count; }
static int g_saved_events;
static char g_saved_path[300];
static void on_event(const tgb_event *ev, void *u) {
(void)u;
if (ev->type == TGB_EVENT_DOCUMENT_SAVED) { g_saved_events++; snprintf(g_saved_path, sizeof g_saved_path, "%s", ev->text ? ev->text : ""); }
}
TEST(save_and_load_file) {
tgb_document *d = new_doc(16, 16);
char p[300]; path_in(p, sizeof p, "proyecto.tgb");
CHECK_OK(paint_px(d, 0, 0, 4, 4, RED));
CHECK(tgb_document_dirty(d));
CHECK_STATUS(tgb_document_save(d, NULL, NULL), TGB_ERR_INVALID_ARG); /* sin ruta */
uint32_t sub; CHECK_OK(tgb_events_subscribe(g_ctx, on_event, NULL, &sub));
g_saved_events = 0;
CHECK_OK(tgb_document_save(d, p, NULL));
CHECK(!tgb_document_dirty(d));
CHECK(tgb_document_path(d) && !strcmp(tgb_document_path(d), p));
CHECK_EQ(g_saved_events, 1);
CHECK(!strcmp(g_saved_path, p));
CHECK_EQ(count_tmp_files(), 0);
tgb_document *r = NULL;
CHECK_OK(tgb_document_load(g_ctx, p, &r));
if (r) { CHECK(is_px(tgb_frame_cel(r, 0, 0), 4, 4, RED)); CHECK(!strcmp(tgb_document_path(r), p)); CHECK(!tgb_document_dirty(r)); tgb_document_destroy(r); }
/* guardar de nuevo usando la ruta asociada */
CHECK_OK(paint_px(d, 0, 0, 5, 5, BLUE));
CHECK_OK(tgb_document_save(d, NULL, NULL));
CHECK_OK(tgb_document_load(g_ctx, p, &r));
if (r) { CHECK(is_px(tgb_frame_cel(r, 0, 0), 5, 5, BLUE)); tgb_document_destroy(r); }
/* no se guarda con una edición a medias */
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "x"));
CHECK_STATUS(tgb_document_save(d, p, NULL), TGB_ERR_STATE);
CHECK_OK(tgb_edit_cancel(d));
/* la exportación a formato 2 no cambia la ruta ni marca como guardado */
CHECK_OK(paint_px(d, 0, 0, 6, 6, GREEN));
char p2[300]; path_in(p2, sizeof p2, "legado.tgb");
tgb_save_options o; tgb_save_options_defaults(&o); o.legacy_v2 = true;
CHECK_OK(tgb_document_save(d, p2, &o));
CHECK(tgb_document_dirty(d) && !strcmp(tgb_document_path(d), p));
CHECK_OK(tgb_events_unsubscribe(g_ctx, sub));
tgb_document_destroy(d);
}
TEST(failed_save_never_damages_existing_file) {
tgb_document *d = new_doc(64, 64);
char p[300]; path_in(p, sizeof p, "valioso.tgb");
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
CHECK_OK(tgb_document_save(d, p, NULL));
size_t before_len; uint8_t *before = slurp(p, &before_len);
CHECK(before != NULL);
/* Simula "disco lleno / no cabe": límite de tamaño de archivo para este proceso */
for (int i = 0; i < 60; i++) { tgb_canvas_set(NULL, 0, 0, RED); CHECK_OK(paint_px(d, 0, 0, i, 10, BLUE)); }
signal(SIGXFSZ, SIG_IGN);
struct rlimit old, lim;
getrlimit(RLIMIT_FSIZE, &old);
lim = old; lim.rlim_cur = 100;
setrlimit(RLIMIT_FSIZE, &lim);
tgb_status s = tgb_document_save(d, p, NULL);
setrlimit(RLIMIT_FSIZE, &old);
CHECK_STATUS(s, TGB_ERR_IO);
CHECK(strlen(tgb_last_error()) > 5);
CHECK(tgb_document_dirty(d)); /* no se declara guardado lo que falló */
size_t after_len; uint8_t *after = slurp(p, &after_len);
CHECK(after && after_len == before_len && memcmp(before, after, before_len) == 0); /* el archivo anterior sigue idéntico */
CHECK_EQ(count_tmp_files(), 0); /* no quedan temporales */
tgb_document *r = NULL;
CHECK_OK(tgb_document_load(g_ctx, p, &r)); /* y sigue siendo un proyecto válido */
if (r) tgb_document_destroy(r);
free(before); free(after);
/* destino imposible: carpeta inexistente / el destino es una carpeta */
char bad[300]; path_in(bad, sizeof bad, "no_existe/dentro.tgb");
CHECK_STATUS(tgb_document_save(d, bad, NULL), TGB_ERR_IO);
char dirp[300]; path_in(dirp, sizeof dirp, "es_carpeta.tgb"); mkdir(dirp, 0700);
CHECK_STATUS(tgb_document_save(d, dirp, NULL), TGB_ERR_IO);
CHECK(access(dirp, F_OK) == 0);
CHECK_EQ(count_tmp_files(), 0);
CHECK(tgb_document_dirty(d));
/* abrir lo que no se puede abrir */
CHECK_STATUS(tgb_document_load(g_ctx, "/tmp/tgb-no-existe-xyz.tgb", &r), TGB_ERR_NOT_FOUND);
CHECK_STATUS(tgb_document_load(g_ctx, dirp, &r), TGB_ERR_IO);
char junk[300]; path_in(junk, sizeof junk, "basura.tgb"); spit(junk, "\x00\x01\x02 no soy json", 15);
CHECK_STATUS(tgb_document_load(g_ctx, junk, &r), TGB_ERR_FORMAT);
CHECK(strstr(tgb_last_error(), "basura.tgb") != NULL); /* el mensaje dice qué archivo falló */
CHECK(r == NULL);
tgb_document_destroy(d);
}
TEST(import_png_as_frame) {
char img[300]; path_in(img, sizeof img, "entrada.png");
spit(img, png_fixtures[0].png, png_fixtures[0].len);
tgb_document *d = new_doc(8, 6);
size_t n0 = undo_count(d);
uint32_t fid = 0;
CHECK_OK(tgb_import_png(d, img, 1, 0, &fid));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK(tgb_frame_id(d, 1) == fid);
CHECK_EQ(undo_count(d), n0 + 1); /* una sola operación deshacible */
CHECK(!strcmp(tgb_history_undo_label(d), "Importar imagen"));
bool same = true; /* centrada: desplazamiento (2,1) */
for (int y = 0; y < FIX_H; y++) for (int x = 0; x < FIX_W; x++) {
tgb_color p = px_at(d, 1, 0, 2 + x, 1 + y);
const uint8_t *e = png_fixtures[0].px + (y * FIX_W + x) * 4;
if (p.r != e[0] || p.g != e[1] || p.b != e[2] || p.a != e[3]) same = false;
}
CHECK(same);
CHECK_EQ(px_at(d, 1, 0, 0, 0).a, 0);
CHECK_OK(tgb_history_undo(d)); CHECK_EQ(tgb_frame_count(d), 1);
CHECK_OK(tgb_history_redo(d)); CHECK_EQ(tgb_frame_count(d), 2);
tgb_document_destroy(d);
/* imagen mayor que el documento: se recorta centrada */
d = new_doc(2, 2);
CHECK_OK(tgb_import_png(d, img, 0, 0, NULL));
const uint8_t *e = png_fixtures[0].px + (0 * FIX_W + 1) * 4; /* el píxel (1,0) de la imagen cae en (0,0) */
tgb_color p = px_at(d, 0, 0, 0, 0);
CHECK(p.r == e[0] && p.g == e[1] && p.a == e[3]);
tgb_document_destroy(d);
}
TEST(import_failures_leave_document_untouched) {
char img[300]; path_in(img, sizeof img, "entrada.png");
spit(img, png_fixtures[0].png, png_fixtures[0].len);
tgb_document *d = new_doc(8, 6);
size_t n0 = undo_count(d);
CHECK_OK(tgb_layer_set_locked(d, 0, true));
n0 = undo_count(d);
CHECK_STATUS(tgb_import_png(d, img, 1, 0, NULL), TGB_ERR_LOCKED);
CHECK_OK(tgb_history_undo(d)); /* desbloquea */
n0 = undo_count(d);
CHECK_STATUS(tgb_import_png(d, img, 1, 5, NULL), TGB_ERR_RANGE);
CHECK_STATUS(tgb_import_png(d, img, 9, 0, NULL), TGB_ERR_RANGE);
CHECK_STATUS(tgb_import_png(d, "/tmp/tgb-no-existe.png", 1, 0, NULL), TGB_ERR_NOT_FOUND);
char bad[300]; path_in(bad, sizeof bad, "mala.png");
spit(bad, "no soy un png", 13);
CHECK_STATUS(tgb_import_png(d, bad, 1, 0, NULL), TGB_ERR_FORMAT);
CHECK(strstr(tgb_last_error(), "mala.png") != NULL);
char trunc[300]; path_in(trunc, sizeof trunc, "truncada.png");
spit(trunc, png_fixtures[0].png, png_fixtures[0].len - 20);
CHECK_STATUS(tgb_import_png(d, trunc, 1, 0, NULL), TGB_ERR_CORRUPT);
CHECK_STATUS(tgb_import_png(d, g_root, 1, 0, NULL), TGB_ERR_IO); /* una carpeta */
CHECK_EQ(tgb_frame_count(d), 1);
CHECK_EQ(undo_count(d), n0);
CHECK(tgb_document_is_consistent(d));
tgb_document_destroy(d);
}
TEST(export_png_and_sequence) {
tgb_document *d = new_doc(20, 10);
CHECK_OK(tgb_layer_add(d, 1, "Arriba", NULL));
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(paint_px(d, 0, 0, 3, 3, RED));
CHECK_OK(paint_px(d, 0, 1, 3, 3, BLUE));
CHECK_OK(paint_px(d, 1, 0, 7, 7, GREEN));
char p[300]; path_in(p, sizeof p, "salida.png");
CHECK_OK(tgb_export_png(d, 0, p));
size_t len; uint8_t *bytes = slurp(p, &len);
tgb_canvas *back = NULL, *flat = tgb_canvas_create(20, 10);
CHECK_OK(tgb_png_decode_memory(bytes, len, &back));
CHECK_OK(tgb_render_flatten(d, 0, flat));
CHECK(back && tgb_canvas_equal(back, flat));
CHECK(back && is_px(back, 3, 3, BLUE)); /* se exporta lo VISIBLE: la capa de arriba */
CHECK_OK(tgb_layer_set_visible(d, 1, false));
CHECK_OK(tgb_export_png(d, 0, p)); /* sobrescribe de forma atómica */
free(bytes); bytes = slurp(p, &len);
tgb_canvas_release(back); back = NULL;
CHECK_OK(tgb_png_decode_memory(bytes, len, &back));
CHECK(is_px(back, 3, 3, RED));
tgb_canvas_release(back); tgb_canvas_release(flat); free(bytes);
CHECK_STATUS(tgb_export_png(d, 9, p), TGB_ERR_RANGE);
CHECK_STATUS(tgb_export_png(NULL, 0, p), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_export_png(d, 0, "/tmp/tgb-carpeta-inexistente/x.png"), TGB_ERR_IO);
/* secuencia */
char dir[300]; path_in(dir, sizeof dir, "frames"); mkdir(dir, 0700);
CHECK_OK(tgb_export_png_sequence(d, dir, "anim"));
char f1[340], f2[340], f3[340];
snprintf(f1, sizeof f1, "%s/anim_0001.png", dir); snprintf(f2, sizeof f2, "%s/anim_0002.png", dir); snprintf(f3, sizeof f3, "%s/anim_0003.png", dir);
CHECK(access(f1, F_OK) == 0 && access(f2, F_OK) == 0 && access(f3, F_OK) != 0);
CHECK_STATUS(tgb_export_png_sequence(d, dir, "../malo"), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_export_png_sequence(d, dir, ""), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_export_png_sequence(d, "/tmp/tgb-carpeta-inexistente", "a"), TGB_ERR_NOT_FOUND);
tgb_document_destroy(d);
}
static uint32_t be32(const uint8_t *p) { return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3]; }
TEST(export_apng_structure) {
tgb_document *d = new_doc(12, 8);
CHECK_OK(tgb_frame_add(d, 1, NULL)); CHECK_OK(tgb_frame_add(d, 2, NULL));
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED)); CHECK_OK(paint_px(d, 1, 0, 2, 2, GREEN)); CHECK_OK(paint_px(d, 2, 0, 3, 3, BLUE));
CHECK_OK(tgb_frame_set_duration(d, 0, 100)); CHECK_OK(tgb_frame_set_duration(d, 1, 250)); CHECK_OK(tgb_frame_set_duration(d, 2, 40));
char p[300]; path_in(p, sizeof p, "anim.apng");
CHECK_OK(tgb_export_apng(d, p));
size_t len; uint8_t *b = slurp(p, &len);
CHECK(b && len > 60 && !memcmp(b, "\x89PNG\r\n\x1a\n", 8));
size_t pos = 8; int fctl = 0, idat = 0, fdat = 0, actl = 0; uint32_t expect_seq = 0;
uint32_t delays[3] = {0}; bool crc_ok = true, order_ok = true;
uint8_t *payload[3] = {NULL}; size_t plen[3] = {0};
uint8_t ihdr[13]; memcpy(ihdr, b + 16, 13);
while (b && pos + 12 <= len) {
uint32_t cl = be32(b + pos); const uint8_t *t = b + pos + 4, *cd = b + pos + 8;
uLong crc = crc32(crc32(0, t, 4), cd, cl);
if ((uint32_t)crc != be32(cd + cl)) crc_ok = false;
if (!memcmp(t, "acTL", 4)) { actl++; CHECK(be32(cd) == 3 && be32(cd + 4) == 0); }
else if (!memcmp(t, "fcTL", 4)) {
if (be32(cd) != expect_seq++) order_ok = false;
if (fctl < 3) delays[fctl] = (uint32_t)((cd[20] << 8) | cd[21]);
CHECK(((cd[22] << 8) | cd[23]) == 1000);
CHECK(be32(cd + 4) == 12 && be32(cd + 8) == 8 && cd[24] == 0 && cd[25] == 0);
fctl++;
} else if (!memcmp(t, "IDAT", 4)) { if (fctl != 1) order_ok = false; payload[0] = malloc(cl); memcpy(payload[0], cd, cl); plen[0] = cl; idat++; }
else if (!memcmp(t, "fdAT", 4)) {
if (be32(cd) != expect_seq++) order_ok = false;
fdat++;
payload[fdat] = malloc(cl - 4); memcpy(payload[fdat], cd + 4, cl - 4); plen[fdat] = cl - 4;
}
pos += 12 + cl;
}
CHECK(crc_ok && order_ok);
CHECK(actl == 1 && fctl == 3 && idat == 1 && fdat == 2);
CHECK(delays[0] == 100 && delays[1] == 250 && delays[2] == 40);
/* cada fotograma, reempaquetado como PNG independiente, es exactamente el fotograma del documento */
for (int i = 0; i < 3; i++) {
uint8_t *png = malloc(8 + 25 + 12 + plen[i] + 12);
size_t o = 0;
memcpy(png, "\x89PNG\r\n\x1a\n", 8); o = 8;
uint8_t hdr[8] = {0, 0, 0, 13, 'I', 'H', 'D', 'R'};
memcpy(png + o, hdr, 8); memcpy(png + o + 8, ihdr, 13);
uLong c = crc32(crc32(0, (const Bytef *)"IHDR", 4), ihdr, 13); png[o + 21] = (uint8_t)(c >> 24); png[o + 22] = (uint8_t)(c >> 16); png[o + 23] = (uint8_t)(c >> 8); png[o + 24] = (uint8_t)c; o += 25;
uint8_t dh[8] = {(uint8_t)(plen[i] >> 24), (uint8_t)(plen[i] >> 16), (uint8_t)(plen[i] >> 8), (uint8_t)plen[i], 'I', 'D', 'A', 'T'};
memcpy(png + o, dh, 8); memcpy(png + o + 8, payload[i], plen[i]);
c = crc32(crc32(0, (const Bytef *)"IDAT", 4), payload[i], (uInt)plen[i]); o += 8 + plen[i];
png[o] = (uint8_t)(c >> 24); png[o + 1] = (uint8_t)(c >> 16); png[o + 2] = (uint8_t)(c >> 8); png[o + 3] = (uint8_t)c; o += 4;
uint8_t end[12] = {0, 0, 0, 0, 'I', 'E', 'N', 'D', 0xAE, 0x42, 0x60, 0x82};
memcpy(png + o, end, 12); o += 12;
tgb_canvas *got = NULL, *want = tgb_canvas_create(12, 8);
CHECK_OK(tgb_png_decode_memory(png, o, &got));
CHECK_OK(tgb_render_flatten(d, (size_t)i, want));
CHECK(got && tgb_canvas_equal(got, want));
tgb_canvas_release(got); tgb_canvas_release(want); free(png); free(payload[i]);
}
free(b);
tgb_document_destroy(d);
}
TEST(write_artifacts_for_external_verification) {
const char *dir = getenv("TGB_TEST_ARTIFACTS");
if (!dir) return; /* opcional: lo usa tools/verify-external.py */
tgb_document *d = new_doc(48, 32);
CHECK_OK(tgb_layer_add(d, 1, "Tinta", NULL));
CHECK_OK(tgb_frame_add(d, 1, NULL)); CHECK_OK(tgb_frame_add(d, 2, NULL));
tgb_brush br = tgb_brush_soft(RED, 9, 0.5f);
for (size_t f = 0; f < 3; f++) {
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, f), tgb_layer_id(d, f % 2), "arte"));
tgb_draw_dab(tgb_edit_canvas(d), &br, 10.0f + (float)f * 12, 16);
tgb_draw_line(tgb_edit_canvas(d), &br, 2, 2, 45, 28);
CHECK_OK(tgb_edit_end(d));
br.color.g = (uint8_t)(80 * f);
}
CHECK_OK(tgb_frame_set_duration(d, 0, 100)); CHECK_OK(tgb_frame_set_duration(d, 1, 250)); CHECK_OK(tgb_frame_set_duration(d, 2, 40));
char p[600];
snprintf(p, sizeof p, "%s/muestra_f0.png", dir); CHECK_OK(tgb_export_png(d, 0, p));
snprintf(p, sizeof p, "%s/muestra.apng", dir); CHECK_OK(tgb_export_apng(d, p));
snprintf(p, sizeof p, "%s/muestra_v3.tgb", dir); CHECK_OK(tgb_document_save(d, p, NULL));
tgb_save_options o; tgb_save_options_defaults(&o); o.legacy_v2 = true;
snprintf(p, sizeof p, "%s/muestra_v2.tgb", dir); CHECK_OK(tgb_document_save(d, p, &o));
tgb_document_destroy(d);
}
int main(void) {
g_ctx = tgb_test_context(g_root, sizeof g_root);
RUN(save_and_load_file);
RUN(failed_save_never_damages_existing_file);
RUN(import_png_as_frame);
RUN(import_failures_leave_document_untouched);
RUN(export_png_and_sequence);
RUN(export_apng_structure);
RUN(write_artifacts_for_external_verification);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}
+450
View File
@@ -0,0 +1,450 @@
/* Base64, JSON, PNG y proyectos .tgb (formatos 2 y 3). */
#include <dirent.h>
#include "tgb_test.h"
#include "fixtures_png.h"
#include "../src/io/io_internal.h"
static tgb_context *g_ctx;
static char g_root[200];
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;
}
/* ---------------- Base64 ---------------- */
TEST(base64_roundtrip_and_errors) {
srand(7);
for (size_t n = 0; n <= 40; n++) {
uint8_t in[40]; for (size_t i = 0; i < n; i++) in[i] = (uint8_t)rand();
char enc[100]; tgb_b64_encode(in, n, enc);
CHECK_EQ(strlen(enc), tgb_b64_encoded_len(n));
uint8_t *dec = NULL; size_t dl = 0;
if (n == 0) { CHECK_STATUS(tgb_b64_decode(enc, 0, 100, &dec, &dl), TGB_ERR_CORRUPT); continue; }
CHECK_OK(tgb_b64_decode(enc, strlen(enc), 100, &dec, &dl));
CHECK(dl == n && memcmp(dec, in, n) == 0);
free(dec);
}
uint8_t *d; size_t l;
CHECK_STATUS(tgb_b64_decode("QUJD", 4, 100, &d, &l), TGB_OK); free(d);
CHECK_STATUS(tgb_b64_decode("QUJ", 3, 100, &d, &l), TGB_ERR_CORRUPT); /* longitud */
CHECK_STATUS(tgb_b64_decode("QU!D", 4, 100, &d, &l), TGB_ERR_CORRUPT); /* carácter */
CHECK_STATUS(tgb_b64_decode("QU D", 4, 100, &d, &l), TGB_ERR_CORRUPT); /* espacio */
CHECK_STATUS(tgb_b64_decode("=UJD", 4, 100, &d, &l), TGB_ERR_CORRUPT); /* relleno al inicio */
CHECK_STATUS(tgb_b64_decode("QU=D", 4, 100, &d, &l), TGB_ERR_CORRUPT);
CHECK_STATUS(tgb_b64_decode("QUJD====", 8, 100, &d, &l), TGB_ERR_CORRUPT); /* relleno en medio */
CHECK_STATUS(tgb_b64_decode("QUJDREVG", 8, 3, &d, &l), TGB_ERR_LIMIT); /* más que el máximo */
const char *url = "data:image/png;base64,QUJD";
CHECK_OK(tgb_b64_decode(url, strlen(url), 100, &d, &l)); CHECK(l == 3 && !memcmp(d, "ABC", 3)); free(d);
}
/* ---------------- JSON ---------------- */
static tgb_status parse_str(const char *s, jnode **r) { return json_parse(s, strlen(s), r); }
TEST(json_valid_documents) {
jnode *r = NULL;
CHECK_OK(parse_str(" {\"a\": [1, -2.5e1, true, false, null, \"x\\n\\u00e9\\ud83d\\ude00\"], \"b\": {}} ", &r));
CHECK(r->t == J_OBJ && r->n == 2);
const jnode *a = json_get(r, "a");
CHECK(a && a->t == J_ARR && a->n == 6);
CHECK(a->items[0].num == 1 && a->items[1].num == -25.0 && a->items[2].b && !a->items[3].b && a->items[4].t == J_NULL);
CHECK(a->items[5].slen == 1 + 1 + 2 + 4 && !memcmp(a->items[5].s, "x\n\xc3\xa9\xf0\x9f\x98\x80", 8)); /* escapes y sustitutos UTF-16 */
CHECK(json_get(r, "nada") == NULL);
json_free(r);
long long v;
CHECK_OK(parse_str("12345678901", &r)); CHECK(json_is_int(r, 0, 1LL << 40, &v) && v == 12345678901LL); json_free(r);
CHECK_OK(parse_str("1.5", &r)); CHECK(!json_is_int(r, 0, 100, &v)); json_free(r);
CHECK_OK(parse_str("1e2", &r)); CHECK(json_is_int(r, 0, 1000, &v) && v == 100); json_free(r);
}
TEST(json_invalid_documents) {
const char *bad[] = {"", " ", "{", "[1,]", "[1 2]", "{\"a\":1,}", "{a:1}", "{\"a\" 1}", "[01]", "[1.]", "[.5]", "[+1]", "[1e]", "nul", "tru", "\"abc",
"\"a\\x\"", "\"\\u12\"", "\"\\ud800\"", "\"\\udc00\"", "[1] 2", "{\"a\":1} x", "\"tab\there\"", "[\"\xff\"]", "'a'", "[1,,2]", "{\"a\":}",
"[NaN]", "[Infinity]", "{\"\\u0000\":1}"};
for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) {
jnode *r = NULL;
tgb_status s = parse_str(bad[i], &r);
if (s == TGB_OK) { printf(" se aceptó JSON inválido: %s\n", bad[i]); json_free(r); }
CHECK_STATUS(s, TGB_ERR_FORMAT);
CHECK(r == NULL);
}
char deep[400]; memset(deep, '[', 100); deep[100] = 0;
jnode *r = NULL;
CHECK_STATUS(parse_str(deep, &r), TGB_ERR_FORMAT); /* profundidad excesiva: sin desbordar la pila */
}
/* ---------------- PNG ---------------- */
static tgb_canvas *random_canvas(int w, int h, unsigned seed) {
srand(seed);
tgb_canvas *c = tgb_canvas_create(w, h);
for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) {
tgb_color p = {(uint8_t)rand(), (uint8_t)rand(), (uint8_t)rand(), (uint8_t)(rand() % 4 == 0 ? 0 : rand())};
tgb_canvas_set(c, x, y, p);
}
return c;
}
TEST(png_roundtrip) {
int sizes[][2] = {{1, 1}, {7, 5}, {64, 33}, {200, 120}};
for (size_t i = 0; i < 4; i++) {
tgb_canvas *c = random_canvas(sizes[i][0], sizes[i][1], (unsigned)i + 1);
uint8_t *png; size_t pl; tgb_canvas *back = NULL;
CHECK_OK(tgb_png_encode_memory(c, &png, &pl));
CHECK_OK(tgb_png_decode_memory(png, pl, &back));
CHECK(back && tgb_canvas_equal(c, back));
tgb_canvas_release(back); free(png); tgb_canvas_release(c);
}
tgb_canvas *empty = tgb_canvas_create(30, 30);
uint8_t *png; size_t pl; tgb_canvas *back;
CHECK_OK(tgb_png_encode_memory(empty, &png, &pl));
CHECK_OK(tgb_png_decode_memory(png, pl, &back));
CHECK(tgb_canvas_is_empty(back));
CHECK(pl < 200); /* se comprime muy bien */
tgb_canvas_release(back); free(png); tgb_canvas_release(empty);
}
TEST(png_reads_third_party_files) {
for (size_t i = 0; i < sizeof png_fixtures / sizeof png_fixtures[0]; i++) {
const png_fixture *f = &png_fixtures[i];
tgb_canvas *c = NULL;
tgb_status s = tgb_png_decode_memory(f->png, f->len, &c);
if (s != TGB_OK) { printf(" fixture %s: %s\n", f->name, tgb_last_error()); }
CHECK_OK(s);
if (!c) continue;
CHECK(tgb_canvas_width(c) == FIX_W && tgb_canvas_height(c) == FIX_H);
bool same = true;
for (int y = 0; y < FIX_H; y++) for (int x = 0; x < FIX_W; x++) {
tgb_color p = tgb_canvas_get(c, x, y);
const uint8_t *e = f->px + (y * FIX_W + x) * 4;
if (p.r != e[0] || p.g != e[1] || p.b != e[2] || p.a != e[3]) same = false;
}
if (!same) printf(" fixture %s: píxeles distintos de lo esperado\n", f->name);
CHECK(same);
tgb_canvas_release(c);
}
}
static void fix_crc(uint8_t *png, size_t chunk_off) { /* recalcula el CRC de un chunk (para fabricar casos de prueba) */
uint32_t len = ((uint32_t)png[chunk_off] << 24) | ((uint32_t)png[chunk_off + 1] << 16) | ((uint32_t)png[chunk_off + 2] << 8) | png[chunk_off + 3];
uint32_t crc = 0xFFFFFFFFu;
for (size_t i = 0; i < len + 4; i++) {
crc ^= png[chunk_off + 4 + i];
for (int k = 0; k < 8; k++) crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u)));
}
crc = ~crc;
size_t o = chunk_off + 8 + len;
png[o] = (uint8_t)(crc >> 24); png[o + 1] = (uint8_t)(crc >> 16); png[o + 2] = (uint8_t)(crc >> 8); png[o + 3] = (uint8_t)crc;
}
TEST(png_rejects_bad_input) {
const png_fixture *f = &png_fixtures[0];
uint8_t *buf = malloc(f->len + 16);
tgb_canvas *c = NULL;
CHECK_STATUS(tgb_png_decode_memory((const uint8_t *)"hola", 4, &c), TGB_ERR_FORMAT);
CHECK_STATUS(tgb_png_decode_memory(NULL, 0, &c), TGB_ERR_INVALID_ARG);
/* truncado en cualquier punto: siempre error, nunca bloqueo ni lectura fuera de rango */
for (size_t n = 0; n < f->len; n++) {
tgb_status s = tgb_png_decode_memory(f->png, n, &c);
if (s == TGB_OK) { tgb_canvas_release(c); printf(" PNG truncado a %zu bytes se aceptó\n", n); }
CHECK(TGB_FAILED(s));
}
/* un byte alterado en los datos: el CRC lo detecta */
memcpy(buf, f->png, f->len);
buf[f->len / 2] ^= 0x55;
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_CORRUPT);
/* entrelazado (Adam7): reconocido y rechazado con mensaje claro */
memcpy(buf, f->png, f->len);
buf[8 + 8 + 12] = 1; fix_crc(buf, 8);
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_UNSUPPORTED);
CHECK(strstr(tgb_last_error(), "entrelazado") != NULL);
/* dimensiones enormes declaradas en IHDR: se rechaza antes de reservar memoria */
memcpy(buf, f->png, f->len);
buf[8 + 8 + 0] = 0x00; buf[8 + 8 + 1] = 0x01; buf[8 + 8 + 2] = 0x86; buf[8 + 8 + 3] = 0xA0; /* ancho = 100000 */
fix_crc(buf, 8);
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_LIMIT);
/* combinación de color/profundidad imposible */
memcpy(buf, f->png, f->len);
buf[8 + 8 + 8] = 4; buf[8 + 8 + 9] = 6; fix_crc(buf, 8);
CHECK_STATUS(tgb_png_decode_memory(buf, f->len, &c), TGB_ERR_CORRUPT);
free(buf);
}
/* ---------------- .tgb ---------------- */
static char *b64_of_png(const png_fixture *f) {
char *s = malloc(tgb_b64_encoded_len(f->len) + 1);
tgb_b64_encode(f->png, f->len, s);
return s;
}
static char *legacy_json(void) {
char *a = b64_of_png(&png_fixtures[0]), *b = b64_of_png(&png_fixtures[1]);
char *j = malloc(strlen(a) + strlen(b) + 400);
sprintf(j, "{\"format\":\"TAHGIBU\",\"version\":2,\"app\":\"Tahgibu Animator\",\"fps\":12,\"width\":4,\"height\":3,"
"\"community_owned\":true,\"frames\":[\"%s\",\"%s\"],\"sprites\":[{\"name\":\"s1\",\"x\":1}]}", a, b);
free(a); free(b);
return j;
}
static tgb_status load_str(const char *json, tgb_document **d) {
return tgb_document_load_memory(g_ctx, (const uint8_t *)json, strlen(json), d);
}
TEST(tgb_legacy_v2_load) {
char *j = legacy_json();
tgb_document *d = NULL;
CHECK_OK(load_str(j, &d));
CHECK(d != NULL);
if (d) {
CHECK(tgb_document_width(d) == 4 && tgb_document_height(d) == 3 && tgb_document_fps(d) == 12);
CHECK_EQ(tgb_frame_count(d), 2);
CHECK_EQ(tgb_layer_count(d), 1);
CHECK_EQ(tgb_frame_duration_ms(d, 0), 83);
const uint8_t *e = png_fixtures[0].px;
tgb_color p = px_at(d, 0, 0, 2, 1);
CHECK(p.r == e[(1 * 4 + 2) * 4] && p.g == e[(1 * 4 + 2) * 4 + 1] && p.a == e[(1 * 4 + 2) * 4 + 3]);
CHECK(!tgb_document_dirty(d));
/* campos desconocidos conservados */
CHECK(tgb_document_extra_field(d, "community_owned") && !strcmp(tgb_document_extra_field(d, "community_owned"), "true"));
CHECK(tgb_document_extra_field(d, "sprites") && !strcmp(tgb_document_extra_field(d, "sprites"), "[{\"name\":\"s1\",\"x\":1}]"));
CHECK(tgb_document_extra_field(d, "format") == NULL);
/* guardar (formato 3) y volver a cargar conserva todo */
uint8_t *out; size_t ol;
CHECK_OK(tgb_document_save_memory(d, NULL, &out, &ol));
CHECK(strstr((char *)out, "\"version\":3") != NULL && strstr((char *)out, "\"community_owned\":true") != NULL);
tgb_document *d2 = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, out, ol, &d2));
if (d2) {
CHECK(tgb_canvas_equal(tgb_frame_cel(d, 0, 0), tgb_frame_cel(d2, 0, 0)) && tgb_canvas_equal(tgb_frame_cel(d, 1, 0), tgb_frame_cel(d2, 1, 0)));
CHECK(!strcmp(tgb_document_extra_field(d2, "sprites"), tgb_document_extra_field(d, "sprites")));
tgb_document_destroy(d2);
}
free(out);
/* exportación explícita al formato 2: "frames" aplanados, sin 'tgb3', mismos campos extra */
tgb_save_options o; tgb_save_options_defaults(&o); o.legacy_v2 = true;
CHECK_OK(tgb_document_save_memory(d, &o, &out, &ol));
CHECK(strstr((char *)out, "\"version\":2") && !strstr((char *)out, "tgb3") && strstr((char *)out, "\"sprites\":[{"));
CHECK_OK(tgb_document_load_memory(g_ctx, out, ol, &d2));
if (d2) { CHECK_EQ(tgb_frame_count(d2), 2); CHECK(tgb_canvas_equal(tgb_frame_cel(d, 0, 0), tgb_frame_cel(d2, 0, 0))); tgb_document_destroy(d2); }
free(out);
tgb_document_destroy(d);
}
free(j);
}
TEST(tgb_v3_roundtrip_full) {
tgb_document *d = new_doc(32, 24);
CHECK_OK(tgb_layer_add(d, 1, "Tinta \"fina\" ñ", NULL));
CHECK_OK(tgb_layer_set_opacity(d, 1, 100));
CHECK_OK(tgb_layer_set_locked(d, 0, true));
CHECK_OK(tgb_layer_set_locked(d, 0, false));
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(paint_px(d, 0, 0, 3, 3, RED));
CHECK_OK(paint_px(d, 1, 1, 8, 8, BLUE));
CHECK_OK(tgb_frame_duplicate(d, 0, NULL)); /* comparte la celda con el fotograma 0 */
CHECK_OK(tgb_frame_set_duration(d, 1, 250));
CHECK_OK(tgb_layer_set_visible(d, 1, false));
uint32_t pa = 0, pb = 0;
CHECK_OK(tgb_recovery_point_create(d, "Antes del azul — ñandú", &pa));
CHECK_OK(paint_px(d, 2, 0, 20, 10, GREEN));
CHECK_OK(tgb_recovery_point_create(d, "Final", &pb));
uint8_t *data; size_t len;
CHECK_OK(tgb_document_save_memory(d, NULL, &data, &len));
tgb_document *r = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, data, len, &r));
CHECK(r != NULL);
if (r) {
CHECK(tgb_document_width(r) == 32 && tgb_document_height(r) == 24 && tgb_document_fps(r) == 12);
CHECK_EQ(tgb_frame_count(r), tgb_frame_count(d));
CHECK_EQ(tgb_layer_count(r), 2);
CHECK(!strcmp(tgb_layer_name(r, 1), "Tinta \"fina\" ñ"));
CHECK(!tgb_layer_visible(r, 1) && tgb_layer_visible(r, 0) && tgb_layer_opacity(r, 1) == 100 && !tgb_layer_locked(r, 0));
for (size_t f = 0; f < tgb_frame_count(d); f++) {
CHECK_EQ(tgb_frame_id(r, f), tgb_frame_id(d, f));
CHECK_EQ(tgb_frame_duration_ms(r, f), tgb_frame_duration_ms(d, f));
for (size_t l = 0; l < 2; l++) {
const tgb_canvas *a = tgb_frame_cel(d, f, l), *b = tgb_frame_cel(r, f, l);
CHECK((a == NULL || tgb_canvas_is_empty(a)) == (b == NULL || tgb_canvas_is_empty(b)));
if (a && b && !tgb_canvas_is_empty(a)) CHECK(tgb_canvas_equal(a, b));
}
}
/* la celda compartida (fotogramas 0 y 1 duplicados... ver ids) sigue compartida tras cargar */
CHECK(tgb_frame_cel(r, 0, 0) == tgb_frame_cel(r, 1, 0) || tgb_frame_cel(r, 0, 0) == tgb_frame_cel(r, 2, 0));
CHECK_EQ(tgb_layer_id(r, 0), tgb_layer_id(d, 0));
/* puntos de recuperación persistentes */
CHECK_EQ(tgb_recovery_point_count(r), 2);
tgb_recovery_point_info a, b;
CHECK_OK(tgb_recovery_point_info_at(r, 0, &a)); CHECK_OK(tgb_recovery_point_info_at(r, 1, &b));
CHECK(a.id == pa && !strcmp(a.name, "Antes del azul — ñandú") && a.compatible && a.created_unix > 1700000000);
CHECK(b.id == pb && !strcmp(b.name, "Final"));
CHECK_OK(tgb_recovery_point_restore(r, pa)); /* se puede volver a un estado anterior tras reabrir */
CHECK_EQ(tgb_frame_count(r), 3);
CHECK_EQ(px_at(r, 2, 0, 20, 10).a, 0); /* el verde aún no existía en ese punto */
CHECK(is_px(tgb_frame_cel(r, 0, 0), 3, 3, RED));
/* los ids nuevos no chocan con los existentes */
tgb_document *r2 = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, data, len, &r2));
uint32_t nid = 0;
CHECK_OK(tgb_frame_add(r2, 0, &nid));
for (size_t f = 1; f < tgb_frame_count(r2); f++) CHECK(tgb_frame_id(r2, f) != nid);
CHECK(nid > tgb_frame_id(d, 2) && nid > tgb_layer_id(d, 1));
tgb_document_destroy(r2);
tgb_document_destroy(r);
}
/* de-duplicación: las celdas compartidas se guardan una sola vez */
jnode *root = NULL;
CHECK_OK(json_parse((char *)data, len, &root));
const jnode *images = json_get(json_get(root, "tgb3"), "images");
CHECK(images && images->t == J_ARR);
CHECK(images->n <= 5); /* 4 celdas distintas como mucho (rojo compartido, azul, verde, + antes) */
json_free(root);
free(data);
/* sin fotogramas aplanados: archivo más pequeño y sin 'frames' */
tgb_save_options o; tgb_save_options_defaults(&o); o.include_flat_frames = false;
uint8_t *small; size_t sl;
CHECK_OK(tgb_document_save_memory(d, &o, &small, &sl));
CHECK(strstr((char *)small, "\"frames\":[\"") == NULL);
tgb_document *r3 = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, small, sl, &r3));
tgb_document_destroy(r3);
free(small);
tgb_document_destroy(d);
}
static void expect_load_fail(const char *what, const char *json, tgb_status expected) {
tgb_document *d = (tgb_document *)0x1;
tgb_status s = load_str(json, &d);
if (s != expected) printf(" [%s] se esperaba %s y devolvió %s (%s)\n", what, tgb_status_name(expected), tgb_status_name(s), tgb_last_error());
CHECK_EQ(s, expected);
CHECK(d == NULL);
CHECK(strlen(tgb_last_error()) > 5); /* mensaje comprensible */
}
TEST(tgb_rejects_corrupt_files) {
char *a = b64_of_png(&png_fixtures[0]);
char buf[8000];
expect_load_fail("vacío", "", TGB_ERR_FORMAT);
expect_load_fail("no json", "esto no es json", TGB_ERR_FORMAT);
expect_load_fail("raíz lista", "[1,2,3]", TGB_ERR_FORMAT);
expect_load_fail("sin format", "{\"version\":2}", TGB_ERR_FORMAT);
expect_load_fail("otro format", "{\"format\":\"OTRO\",\"version\":2}", TGB_ERR_FORMAT);
expect_load_fail("sin version", "{\"format\":\"TAHGIBU\"}", TGB_ERR_FORMAT);
expect_load_fail("version futura", "{\"format\":\"TAHGIBU\",\"version\":99,\"fps\":12,\"width\":4,\"height\":3}", TGB_ERR_INCOMPATIBLE);
expect_load_fail("version 1", "{\"format\":\"TAHGIBU\",\"version\":1,\"fps\":12,\"width\":4,\"height\":3,\"frames\":[]}", TGB_ERR_UNSUPPORTED);
expect_load_fail("version decimal", "{\"format\":\"TAHGIBU\",\"version\":2.5}", TGB_ERR_FORMAT);
const char *head = "{\"format\":\"TAHGIBU\",\"version\":2,";
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":0,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("ancho 0", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":5000,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("ancho enorme", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":0,\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("fps 0", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":100,\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("fps 100", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":\"12\",\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("fps texto", buf, TGB_ERR_RANGE);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3}", head); expect_load_fail("sin frames", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[]}", head); expect_load_fail("frames vacío", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":\"x\"}", head); expect_load_fail("frames no lista", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[7]}", head); expect_load_fail("frame no cadena", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[\"@@@@\"]}", head); expect_load_fail("base64 malo", buf, TGB_ERR_CORRUPT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[\"QUJDRA==\"]}", head); expect_load_fail("no es PNG", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":9,\"height\":3,\"frames\":[\"%s\"]}", head, a); expect_load_fail("dimensiones distintas", buf, TGB_ERR_FORMAT);
/* PNG con un byte dañado */
uint8_t *png = malloc(png_fixtures[0].len);
memcpy(png, png_fixtures[0].png, png_fixtures[0].len); png[png_fixtures[0].len / 2] ^= 1;
char *bad = malloc(tgb_b64_encoded_len(png_fixtures[0].len) + 1); tgb_b64_encode(png, png_fixtures[0].len, bad);
snprintf(buf, sizeof buf, "%s\"fps\":12,\"width\":4,\"height\":3,\"frames\":[\"%s\"]}", head, bad); expect_load_fail("PNG dañado", buf, TGB_ERR_CORRUPT);
free(png); free(bad);
/* demasiada memoria: se rechaza ANTES de decodificar nada */
char *many = malloc(40000); strcpy(many, "[");
for (int i = 0; i < 600; i++) strcat(many, i ? ",\"AAAA\"" : "\"AAAA\"");
strcat(many, "]");
char *big = malloc(strlen(many) + 200);
sprintf(big, "%s\"fps\":12,\"width\":4096,\"height\":4096,\"frames\":%s}", head, many);
expect_load_fail("bomba de memoria", big, TGB_ERR_LIMIT);
free(many); free(big);
/* formato 3 */
const char *h3 = "{\"format\":\"TAHGIBU\",\"version\":3,\"fps\":12,\"width\":4,\"height\":3,";
snprintf(buf, sizeof buf, "%s\"frames\":[]}", h3); expect_load_fail("v3 sin tgb3", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],\"layers\":[],\"frames\":[]}}", h3); expect_load_fail("v3 sin capas", buf, TGB_ERR_FORMAT);
const char *lay = "\"layers\":[{\"id\":1,\"name\":\"L\"}]";
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[]}]}}", h3, lay); expect_load_fail("cels de menos", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[3]}]}}", h3, lay); expect_load_fail("imagen inexistente", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":5,\"cels\":[null]}]}}", h3, lay); expect_load_fail("duración mala", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]},{\"id\":2,\"duration_ms\":83,\"cels\":[null]}]}}", h3, lay); expect_load_fail("ids repetidos", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[],\"layers\":[{\"id\":1,\"name\":\"\"}],\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]}]}}", h3); expect_load_fail("capa sin nombre", buf, TGB_ERR_FORMAT);
snprintf(buf, sizeof buf, "%s\"tgb3\":{\"images\":[\"%s\"],%s,\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]}]}}", h3, "QUJDRA==", lay); expect_load_fail("imagen no PNG", buf, TGB_ERR_FORMAT);
free(a);
}
TEST(tgb_incompatible_recovery_point_is_preserved) {
const char *json = "{\"format\":\"TAHGIBU\",\"version\":3,\"fps\":12,\"width\":4,\"height\":3,\"tgb3\":{\"images\":[],"
"\"layers\":[{\"id\":1,\"name\":\"L\"}],\"frames\":[{\"id\":2,\"duration_ms\":83,\"cels\":[null]}],\"next_id\":3,\"recovery_points\":["
"{\"id\":5,\"name\":\"Futuro\",\"created\":1700000000,\"state_version\":7,\"state\":{\"nuevo\":[1,2,3]}},"
"{\"id\":6,\"name\":\"Roto\",\"state_version\":1,\"state\":{\"layers\":[],\"frames\":[]}}]}}";
tgb_document *d = NULL;
CHECK_OK(load_str(json, &d));
CHECK(d != NULL);
if (!d) return;
CHECK_EQ(tgb_recovery_point_count(d), 2);
tgb_recovery_point_info a, b;
CHECK_OK(tgb_recovery_point_info_at(d, 0, &a)); CHECK_OK(tgb_recovery_point_info_at(d, 1, &b));
CHECK(a.id == 5 && !a.compatible && !strcmp(a.name, "Futuro"));
CHECK(b.id == 6 && !b.compatible);
CHECK_STATUS(tgb_recovery_point_restore(d, 5), TGB_ERR_INCOMPATIBLE); /* no se restaura... */
CHECK_EQ(tgb_frame_count(d), 1);
CHECK(!tgb_history_can_undo(d)); /* ...y el documento no se toca */
CHECK_OK(tgb_recovery_point_create(d, "Nuevo", NULL)); /* los ids no chocan con los existentes */
uint8_t *out; size_t ol;
CHECK_OK(tgb_document_save_memory(d, NULL, &out, &ol)); /* ...y se conservan al guardar */
CHECK(strstr((char *)out, "\"state_version\":7,\"state\":{\"nuevo\":[1,2,3]}") != NULL);
tgb_document *r = NULL;
CHECK_OK(tgb_document_load_memory(g_ctx, out, ol, &r));
if (r) { CHECK_EQ(tgb_recovery_point_count(r), 3); tgb_document_destroy(r); }
free(out);
tgb_document_destroy(d);
}
TEST(tgb_fuzz_truncation_and_bitflips) {
tgb_document *d = new_doc(24, 16);
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(tgb_layer_add(d, 1, "B", NULL));
CHECK_OK(paint_px(d, 0, 0, 2, 2, RED));
CHECK_OK(paint_px(d, 1, 1, 5, 5, BLUE));
CHECK_OK(tgb_recovery_point_create(d, "P", NULL));
uint8_t *data; size_t len;
CHECK_OK(tgb_document_save_memory(d, NULL, &data, &len));
int ok_loads = 0, failed = 0;
for (size_t n = 0; n < len; n += 53) { /* truncar en muchos puntos */
tgb_document *r = NULL;
tgb_status s = tgb_document_load_memory(g_ctx, data, n, &r);
if (s == TGB_OK) { ok_loads++; tgb_document_destroy(r); } else { failed++; CHECK(r == NULL); }
}
CHECK(failed > 10);
srand(99);
uint8_t *copy = malloc(len);
for (int i = 0; i < 400; i++) { /* byte alterado al azar */
memcpy(copy, data, len);
copy[(size_t)rand() % len] = (uint8_t)rand();
tgb_document *r = NULL;
tgb_status s = tgb_document_load_memory(g_ctx, copy, len, &r);
if (s == TGB_OK) { ok_loads++; CHECK(r != NULL); tgb_document_destroy(r); } else { failed++; CHECK(r == NULL); }
}
printf(" (fuzz: %d cargas aceptadas, %d rechazadas, sin fallos de memoria)\n", ok_loads, failed);
free(copy); free(data);
tgb_document_destroy(d);
}
int main(void) {
g_ctx = tgb_test_context(g_root, sizeof g_root);
RUN(base64_roundtrip_and_errors);
RUN(json_valid_documents);
RUN(json_invalid_documents);
RUN(png_roundtrip);
RUN(png_reads_third_party_files);
RUN(png_rejects_bad_input);
RUN(tgb_legacy_v2_load);
RUN(tgb_v3_roundtrip_full);
RUN(tgb_rejects_corrupt_files);
RUN(tgb_incompatible_recovery_point_is_preserved);
RUN(tgb_fuzz_truncation_and_bitflips);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}
+378
View File
@@ -0,0 +1,378 @@
/* 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();
}
+307
View File
@@ -0,0 +1,307 @@
/* Autoguardado cada 5 minutos y recuperación tras cierres inesperados (con procesos reales). */
#include <dirent.h>
#include <signal.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "tgb_test.h"
static tgb_context *g_ctx;
static char g_root[200];
static tgb_document *new_doc(tgb_context *ctx, int w, int h) {
tgb_document *d = NULL;
CHECK_OK(tgb_document_create(ctx, w, h, 12, &d));
return d;
}
static void rec_dir(char *out, size_t cap) { snprintf(out, cap, "%s/state/recovery", g_root); }
static size_t count_files(const char *dir, const char *needle) {
DIR *dp = opendir(dir);
size_t n = 0; struct dirent *e;
while (dp && (e = readdir(dp))) if (strstr(e->d_name, needle)) n++;
if (dp) closedir(dp);
return n;
}
static uint8_t *slurp(const char *path, size_t *len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
uint8_t *b = malloc((size_t)n + 1);
*len = fread(b, 1, (size_t)n, f);
fclose(f);
return b;
}
static void spit(const char *path, const void *data, size_t len) { FILE *f = fopen(path, "wb"); fwrite(data, 1, len, f); fclose(f); }
static void wipe_recovery(void) {
char d[400]; rec_dir(d, sizeof d);
DIR *dp = opendir(d); struct dirent *e;
while (dp && (e = readdir(dp))) { if (e->d_name[0] == '.') continue; char p[700]; snprintf(p, sizeof p, "%s/%s", d, e->d_name); remove(p); }
if (dp) closedir(dp);
}
static int g_autosaved;
static void on_event(const tgb_event *ev, void *u) { (void)u; if (ev->type == TGB_EVENT_AUTOSAVED) g_autosaved++; }
/* Proceso hijo que trabaja y MUERE sin cerrar nada (simula un cierre inesperado). */
static pid_t spawn_crashing_session(const char *saved_path, bool wait_on_pipe, int *go_fd, int *ready_fd) {
int go[2] = {-1, -1}, ready[2] = {-1, -1};
if (wait_on_pipe) { if (pipe(go) || pipe(ready)) exit(2); }
fflush(stdout);
pid_t pid = fork();
if (pid == 0) {
tgb_context *ctx = tgb_test_context_at(g_root);
tgb_recovery *rec = NULL;
if (tgb_recovery_open(ctx, &rec) != TGB_OK) _exit(10);
tgb_document *a = new_doc(ctx, 32, 32);
paint_px(a, 0, 0, 5, 5, RED);
if (saved_path) tgb_document_save(a, saved_path, NULL); /* guardado por el usuario... */
paint_px(a, 0, 0, 6, 6, BLUE); /* ...y un cambio SIN guardar */
tgb_document *b = new_doc(ctx, 16, 16); /* un documento que nunca se guardó */
paint_px(b, 0, 0, 1, 1, GREEN);
tgb_recovery_attach(rec, a, "Mi dibujo");
tgb_recovery_attach(rec, b, "Sin título");
tgb_recovery_set_interval(rec, 0.5);
tgb_recovery_tick(rec, 0.0);
if (tgb_recovery_tick(rec, 1.0) != TGB_OK) _exit(11);
if (wait_on_pipe) { char c = 'r', x; if (write(ready[1], &c, 1) != 1 || read(go[0], &x, 1) != 1) _exit(12); }
_exit(0); /* sin tgb_recovery_close: como un fallo de corriente */
}
if (go_fd) *go_fd = go[1];
if (ready_fd) *ready_fd = ready[0];
return pid;
}
TEST(autosave_every_five_minutes) {
wipe_recovery();
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
tgb_document *d = new_doc(g_ctx, 16, 16);
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
CHECK_OK(tgb_recovery_attach(rec, d, "Prueba"));
CHECK_STATUS(tgb_recovery_attach(rec, d, "Otra vez"), TGB_ERR_EXISTS);
uint32_t sub; g_autosaved = 0;
CHECK_OK(tgb_events_subscribe(g_ctx, on_event, NULL, &sub));
char dir[400]; rec_dir(dir, sizeof dir);
CHECK_OK(tgb_recovery_tick(rec, 1000.0)); /* primer tick: fija la hora de referencia */
CHECK_OK(tgb_recovery_tick(rec, 1299.0));
CHECK_EQ(count_files(dir, ".tgb"), 0); /* aún no han pasado 5 minutos */
CHECK_OK(tgb_recovery_tick(rec, 1300.0));
CHECK_EQ(count_files(dir, "autosave-"), 2); /* .tgb + .json */
CHECK_EQ(g_autosaved, 1);
CHECK_OK(tgb_recovery_tick(rec, 1700.0)); /* sin cambios nuevos: no vuelve a guardar */
CHECK_EQ(g_autosaved, 1);
CHECK_OK(paint_px(d, 0, 0, 2, 2, BLUE));
CHECK_OK(tgb_recovery_tick(rec, 1800.0)); /* 100 s después del último tick: aún no toca */
CHECK_EQ(g_autosaved, 1);
CHECK_OK(tgb_recovery_tick(rec, 2000.0)); /* ya pasaron 300 s desde el último autoguardado fallido/omitido */
CHECK_EQ(g_autosaved, 2);
/* el usuario guarda el proyecto: el autoguardado sobra */
char proj[400]; snprintf(proj, sizeof proj, "%s/p.tgb", g_root);
CHECK_OK(tgb_document_save(d, proj, NULL));
tgb_recovery_notify_saved(rec, d);
CHECK_EQ(count_files(dir, "autosave-"), 0);
CHECK_OK(tgb_recovery_tick(rec, 9000.0)); /* documento limpio: nada que autoguardar */
CHECK_EQ(g_autosaved, 2);
CHECK_OK(paint_px(d, 0, 0, 3, 3, RED));
CHECK_OK(tgb_recovery_save_now(rec, d)); /* guardado inmediato a petición */
CHECK_EQ(count_files(dir, ".tgb"), 1);
CHECK_OK(tgb_recovery_detach(rec, d)); /* cerrar el documento borra su autoguardado */
CHECK_EQ(count_files(dir, "autosave-"), 0);
CHECK_STATUS(tgb_recovery_detach(rec, d), TGB_ERR_NOT_FOUND);
CHECK_OK(tgb_events_unsubscribe(g_ctx, sub));
tgb_recovery_close(rec, true);
CHECK_EQ(count_files(dir, "session-"), 0); /* cierre limpio: no queda marcador */
tgb_document_destroy(d);
}
TEST(tick_waits_while_a_stroke_is_open) {
wipe_recovery();
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
tgb_document *d = new_doc(g_ctx, 16, 16);
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
tgb_recovery_attach(rec, d, "x");
char dir[400]; rec_dir(dir, sizeof dir);
tgb_recovery_tick(rec, 0.0);
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "trazo"));
CHECK_OK(tgb_recovery_tick(rec, 1000.0)); /* trazo a medias: se aplaza sin error */
CHECK_EQ(count_files(dir, ".tgb"), 0);
CHECK_OK(tgb_edit_cancel(d));
CHECK_OK(tgb_recovery_tick(rec, 1001.0)); /* ya se puede */
CHECK_EQ(count_files(dir, ".tgb"), 1);
tgb_recovery_close(rec, true);
tgb_document_destroy(d);
}
TEST(recover_after_unexpected_exit) {
wipe_recovery();
char orig[400]; snprintf(orig, sizeof orig, "%s/original.tgb", g_root);
pid_t pid = spawn_crashing_session(orig, false, NULL, NULL);
int st = 0; waitpid(pid, &st, 0);
CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0);
size_t before_len; uint8_t *before = slurp(orig, &before_len);
CHECK(before != NULL);
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
CHECK_EQ(tgb_recovery_candidate_count(rec), 2); /* los dos documentos de la sesión muerta */
size_t with_orig = 99, without = 99;
for (size_t i = 0; i < 2; i++) {
tgb_recovery_candidate c;
CHECK_OK(tgb_recovery_candidate_at(rec, i, &c));
CHECK(c.saved_unix > 1700000000);
if (!strcmp(c.original_path, orig)) { with_orig = i; CHECK(!strcmp(c.title, "Mi dibujo")); }
else { without = i; CHECK(!strcmp(c.title, "Sin título") && c.original_path[0] == '\0'); }
}
CHECK(with_orig != 99 && without != 99);
tgb_document *r = NULL;
CHECK_OK(tgb_recovery_candidate_load(rec, with_orig, &r));
if (r) {
CHECK(is_px(tgb_frame_cel(r, 0, 0), 5, 5, RED));
CHECK(is_px(tgb_frame_cel(r, 0, 0), 6, 6, BLUE)); /* se recuperó el cambio que NO estaba guardado */
CHECK(tgb_document_dirty(r));
CHECK(tgb_document_path(r) == NULL); /* sin ruta: no puede pisar el original por accidente */
char saveas[400]; snprintf(saveas, sizeof saveas, "%s/recuperado.tgb", g_root);
CHECK_OK(tgb_document_save(r, saveas, NULL));
tgb_document_destroy(r);
}
/* el archivo original no se ha tocado */
size_t after_len; uint8_t *after = slurp(orig, &after_len);
CHECK(after && after_len == before_len && !memcmp(before, after, before_len));
tgb_document *loaded = NULL;
CHECK_OK(tgb_document_load(g_ctx, orig, &loaded));
if (loaded) { CHECK_EQ(px_at(loaded, 0, 0, 6, 6).a, 0); tgb_document_destroy(loaded); } /* el original no tiene el azul */
free(before); free(after);
/* los candidatos sobreviven a un reinicio hasta que el usuario decida */
tgb_recovery_close(rec, true);
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
CHECK_EQ(tgb_recovery_candidate_count(rec), 2);
CHECK_OK(tgb_recovery_candidate_discard(rec, 0));
CHECK_EQ(tgb_recovery_candidate_count(rec), 1);
CHECK_STATUS(tgb_recovery_candidate_discard(rec, 5), TGB_ERR_RANGE);
CHECK_STATUS(tgb_recovery_candidate_load(rec, 5, &r), TGB_ERR_RANGE);
CHECK_OK(tgb_recovery_candidate_discard(rec, 0));
char dir[400]; rec_dir(dir, sizeof dir);
CHECK_EQ(count_files(dir, "autosave-"), 0);
tgb_recovery_close(rec, true);
}
TEST(live_session_is_not_recovered) {
wipe_recovery();
int go = -1, ready = -1;
pid_t pid = spawn_crashing_session(NULL, true, &go, &ready);
char c = 0;
CHECK(read(ready, &c, 1) == 1); /* el hijo ya autoguardó y sigue VIVO */
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
CHECK_EQ(tgb_recovery_candidate_count(rec), 0); /* otra instancia en marcha: sus archivos no son nuestros */
tgb_recovery_close(rec, true);
CHECK(write(go, "x", 1) == 1); /* ahora el hijo "se cae" */
int st; waitpid(pid, &st, 0);
close(go); close(ready);
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
CHECK_EQ(tgb_recovery_candidate_count(rec), 2);
tgb_recovery_close(rec, true);
wipe_recovery();
}
TEST(corrupt_autosave_and_leftovers) {
wipe_recovery();
pid_t pid = spawn_crashing_session(NULL, false, NULL, NULL);
int st; waitpid(pid, &st, 0);
char dir[400]; rec_dir(dir, sizeof dir);
/* PID de un proceso que ya no existe */
pid_t dead = fork(); if (dead == 0) _exit(0); waitpid(dead, &st, 0);
char p[700];
snprintf(p, sizeof p, "%s/autosave-9-9-1.tgb.tmp-%ld", dir, (long)dead); spit(p, "parcial", 7); /* resto de un guardado interrumpido */
snprintf(p, sizeof p, "%s/autosave-9-9-2.tgb.tmp-%ld", dir, (long)getpid()); spit(p, "mio", 3); /* de un proceso vivo: se respeta */
snprintf(p, sizeof p, "%s/session-basura.lock", dir); spit(p, "xx", 2); /* marcador ilegible */
snprintf(p, sizeof p, "%s/session-%ld-1.lock", dir, (long)dead);
char lock[100]; int ll = snprintf(lock, sizeof lock, "{\"pid\":%ld,\"start\":1}", (long)dead); spit(p, lock, (size_t)ll);
/* dañar uno de los autoguardados */
DIR *dp = opendir(dir); struct dirent *e; char victim[700] = "";
while ((e = readdir(dp))) if (strstr(e->d_name, "autosave-") && strstr(e->d_name, ".tgb") && !strstr(e->d_name, ".tmp-") && !victim[0]) snprintf(victim, sizeof victim, "%s/%s", dir, e->d_name);
closedir(dp);
CHECK(victim[0] != 0);
size_t vl; uint8_t *vb = slurp(victim, &vl);
spit(victim, vb, vl / 2); /* truncado */
free(vb);
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
CHECK_EQ(count_files(dir, ".tmp-"), 1); /* solo queda el del proceso vivo */
CHECK_EQ(count_files(dir, "session-basura"), 0); /* marcadores muertos retirados */
CHECK_EQ(tgb_recovery_candidate_count(rec), 2);
int failed = 0, ok = 0;
for (size_t i = 0; i < 2; i++) {
tgb_document *r = NULL;
tgb_status s = tgb_recovery_candidate_load(rec, i, &r);
if (TGB_FAILED(s)) { failed++; CHECK(r == NULL); CHECK(strlen(tgb_last_error()) > 5); } else { ok++; tgb_document_destroy(r); }
}
CHECK_EQ(failed, 1); CHECK_EQ(ok, 1); /* el dañado falla con mensaje claro; el sano se recupera */
CHECK(access(victim, F_OK) == 0); /* el dañado NO se borra solo */
tgb_recovery_close(rec, true);
wipe_recovery();
}
TEST(failed_autosave_keeps_previous_copy) {
wipe_recovery();
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
tgb_document *d = new_doc(g_ctx, 64, 64);
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
tgb_recovery_attach(rec, d, "t");
tgb_recovery_set_interval(rec, 10.0);
char dir[400]; rec_dir(dir, sizeof dir);
tgb_recovery_tick(rec, 0.0);
CHECK_OK(tgb_recovery_tick(rec, 10.0));
char file[700] = "";
DIR *dp = opendir(dir); struct dirent *e;
while ((e = readdir(dp))) if (strstr(e->d_name, ".tgb") && strstr(e->d_name, "autosave-")) snprintf(file, sizeof file, "%s/%s", dir, e->d_name);
closedir(dp);
size_t l1; uint8_t *first = slurp(file, &l1);
CHECK(first != NULL);
for (int i = 0; i < 40; i++) CHECK_OK(paint_px(d, 0, 0, i, 20, BLUE));
signal(SIGXFSZ, SIG_IGN);
struct rlimit old, lim; getrlimit(RLIMIT_FSIZE, &old); lim = old; lim.rlim_cur = 100; setrlimit(RLIMIT_FSIZE, &lim); /* "sin espacio" */
tgb_status s = tgb_recovery_tick(rec, 20.0);
setrlimit(RLIMIT_FSIZE, &old);
CHECK_STATUS(s, TGB_ERR_IO);
CHECK(strstr(tgb_last_error(), "autoguardado") != NULL);
size_t l2; uint8_t *second = slurp(file, &l2);
CHECK(second && l2 == l1 && !memcmp(first, second, l1)); /* la copia anterior sigue intacta y utilizable */
CHECK_EQ(count_files(dir, ".tmp-"), 0);
CHECK_OK(tgb_recovery_tick(rec, 25.0)); /* no reintenta antes de tiempo (ni hace ruido) */
CHECK_OK(tgb_recovery_tick(rec, 30.0)); /* con espacio otra vez, funciona */
uint8_t *third = slurp(file, &l2);
CHECK(third && (l2 != l1 || memcmp(first, third, l1) != 0));
free(first); free(second); free(third);
tgb_recovery_close(rec, true);
tgb_document_destroy(d);
}
TEST(orphan_history_files_are_cleaned) {
wipe_recovery();
char hdir[400]; snprintf(hdir, sizeof hdir, "%s/cache/history", g_root);
mkdir(hdir, 0700);
int st; pid_t dead = fork(); if (dead == 0) _exit(0); waitpid(dead, &st, 0);
char p[700];
snprintf(p, sizeof p, "%s/tgb-hist-%ld-3.bin", hdir, (long)dead); spit(p, "huérfano", 8);
snprintf(p, sizeof p, "%s/tgb-hist-%ld-4.bin", hdir, (long)getpid()); spit(p, "vivo", 4);
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
CHECK_EQ(count_files(hdir, "tgb-hist-"), 1);
tgb_recovery_close(rec, true);
wipe_recovery();
}
int main(void) {
g_ctx = tgb_test_context(g_root, sizeof g_root);
RUN(autosave_every_five_minutes);
RUN(tick_waits_while_a_stroke_is_open);
RUN(recover_after_unexpected_exit);
RUN(live_session_is_not_recovered);
RUN(corrupt_autosave_and_leftovers);
RUN(failed_autosave_keeps_previous_copy);
RUN(orphan_history_files_are_cleaned);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}
+122
View File
@@ -0,0 +1,122 @@
/* Puntos de recuperación nombrados (en memoria; la persistencia se prueba en test_io). */
#include "tgb_test.h"
static tgb_context *g_ctx;
static char g_root[200];
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;
}
TEST(create_list_delete) {
tgb_document *d = new_doc(16, 16);
CHECK_EQ(tgb_recovery_point_count(d), 0);
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
uint64_t rev = tgb_document_revision(d);
uint32_t a = 0, b = 0;
CHECK_OK(tgb_recovery_point_create(d, "Base", &a));
CHECK(tgb_document_revision(d) != rev); /* hay algo nuevo que guardar */
CHECK_OK(paint_px(d, 0, 0, 2, 2, BLUE));
CHECK_OK(tgb_recovery_point_create(d, "Después del azul", &b));
CHECK_EQ(tgb_recovery_point_count(d), 2);
tgb_recovery_point_info info;
CHECK_OK(tgb_recovery_point_info_at(d, 0, &info));
CHECK(info.id == a && !strcmp(info.name, "Base") && info.compatible && info.created_unix > 1700000000);
CHECK_STATUS(tgb_recovery_point_info_at(d, 5, &info), TGB_ERR_RANGE);
CHECK_STATUS(tgb_recovery_point_create(d, "Base", NULL), TGB_ERR_EXISTS);
CHECK_STATUS(tgb_recovery_point_create(d, "", NULL), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_recovery_point_create(d, " ", NULL), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_recovery_point_create(d, NULL, NULL), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_recovery_point_create(d, "\xff\xfe", NULL), TGB_ERR_INVALID_ARG);
CHECK_STATUS(tgb_recovery_point_create(d, "a\nb", NULL), TGB_ERR_INVALID_ARG);
char longname[300]; memset(longname, 'x', sizeof longname - 1); longname[sizeof longname - 1] = 0;
CHECK_STATUS(tgb_recovery_point_create(d, longname, NULL), TGB_ERR_LIMIT);
CHECK_OK(tgb_recovery_point_delete(d, a));
CHECK_EQ(tgb_recovery_point_count(d), 1);
CHECK_STATUS(tgb_recovery_point_delete(d, a), TGB_ERR_NOT_FOUND);
CHECK_STATUS(tgb_recovery_point_restore(d, 9999), TGB_ERR_NOT_FOUND);
CHECK_OK(tgb_edit_begin(d, tgb_frame_id(d, 0), tgb_layer_id(d, 0), "x"));
CHECK_STATUS(tgb_recovery_point_create(d, "Durante edición", NULL), TGB_ERR_STATE);
CHECK_OK(tgb_edit_cancel(d));
tgb_document_destroy(d);
}
TEST(restore_goes_back_and_keeps_safety_point) {
tgb_document *d = new_doc(16, 16);
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
uint32_t base = 0;
CHECK_OK(tgb_recovery_point_create(d, "Base", &base));
CHECK_OK(paint_px(d, 0, 0, 2, 2, BLUE));
CHECK_OK(tgb_frame_add(d, 1, NULL));
CHECK_OK(tgb_layer_add(d, 1, "Otra", NULL));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK_OK(tgb_recovery_point_restore(d, base));
CHECK_EQ(tgb_frame_count(d), 1);
CHECK_EQ(tgb_layer_count(d), 1);
CHECK(is_px(tgb_frame_cel(d, 0, 0), 1, 1, RED));
CHECK_EQ(px_at(d, 0, 0, 2, 2).a, 0);
CHECK(!tgb_history_can_undo(d) && !tgb_history_can_redo(d)); /* el historial ordinario se vacía */
CHECK_EQ(tgb_recovery_point_count(d), 2); /* Base + punto de seguridad */
tgb_recovery_point_info info;
CHECK_OK(tgb_recovery_point_info_at(d, 1, &info));
CHECK(strstr(info.name, "Antes de restaurar") != NULL && strstr(info.name, "Base") != NULL);
/* volver al estado anterior a la restauración usando el punto automático */
CHECK_OK(tgb_recovery_point_restore(d, info.id));
CHECK_EQ(tgb_frame_count(d), 2);
CHECK_EQ(tgb_layer_count(d), 2);
CHECK(is_px(tgb_frame_cel(d, 0, 0), 2, 2, BLUE));
/* tras restaurar se sigue pudiendo editar y deshacer con normalidad */
CHECK_OK(paint_px(d, 0, 0, 9, 9, GREEN));
CHECK_OK(tgb_history_undo(d));
CHECK_EQ(px_at(d, 0, 0, 9, 9).a, 0);
/* restaurar dos veces el mismo punto: nombres de seguridad únicos */
CHECK_OK(tgb_recovery_point_restore(d, base));
CHECK_OK(tgb_recovery_point_restore(d, base));
for (size_t i = 0; i < tgb_recovery_point_count(d); i++)
for (size_t j = i + 1; j < tgb_recovery_point_count(d); j++) {
tgb_recovery_point_info a, b;
tgb_recovery_point_info_at(d, i, &a); tgb_recovery_point_info_at(d, j, &b);
CHECK(strcmp(a.name, b.name) != 0);
}
CHECK(tgb_document_is_consistent(d));
tgb_document_destroy(d);
}
TEST(points_share_memory_copy_on_write) {
tgb_document *d = new_doc(64, 64);
CHECK_OK(paint_px(d, 0, 0, 1, 1, RED));
CHECK_OK(tgb_recovery_point_create(d, "P", NULL));
CHECK_EQ(tgb_recovery_points_extra_bytes(d), 0); /* comparte la celda: coste cero */
CHECK_EQ(tgb_canvas_refs(tgb_frame_cel(d, 0, 0)), 2);
CHECK_OK(paint_px(d, 0, 0, 2, 2, BLUE)); /* editar separa la celda */
CHECK_EQ(tgb_recovery_points_extra_bytes(d), 64 * 64 * 4); /* ahora el punto retiene la versión vieja */
CHECK_OK(tgb_recovery_point_create(d, "Q", NULL)); /* otro punto de la misma versión: no duplica */
CHECK_OK(tgb_recovery_point_create(d, "R", NULL));
CHECK_EQ(tgb_recovery_points_extra_bytes(d), 64 * 64 * 4);
tgb_document_destroy(d);
}
TEST(point_limit) {
tgb_document *d = new_doc(8, 8);
char name[32];
for (int i = 0; i < TGB_MAX_RECOVERY_POINTS; i++) {
snprintf(name, sizeof name, "P%d", i);
CHECK_OK(tgb_recovery_point_create(d, name, NULL));
}
CHECK_STATUS(tgb_recovery_point_create(d, "Uno más", NULL), TGB_ERR_LIMIT); /* nunca borra puntos del usuario */
CHECK_EQ(tgb_recovery_point_count(d), TGB_MAX_RECOVERY_POINTS);
tgb_document_destroy(d);
}
int main(void) {
g_ctx = tgb_test_context(g_root, sizeof g_root);
RUN(create_list_delete);
RUN(restore_goes_back_and_keeps_safety_point);
RUN(points_share_memory_copy_on_write);
RUN(point_limit);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}
+91
View File
@@ -0,0 +1,91 @@
/* Mini marco de pruebas (solo cabecera). Cada CHECK que falla se cuenta e imprime. */
#ifndef TGB_TEST_H
#define TGB_TEST_H
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <ftw.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "tahgibu/tahgibu.h"
#pragma GCC diagnostic ignored "-Wunused-function" /* no todas las pruebas usan todos los ayudantes */
static int g_fail = 0, g_checks = 0;
#define CHECK(cond) do { g_checks++; if (!(cond)) { g_fail++; printf(" FALLO %s:%d: %s\n", __FILE__, __LINE__, #cond); } } while (0)
#define CHECK_EQ(a, b) do { long long _a = (long long)(a), _b = (long long)(b); g_checks++; \
if (_a != _b) { g_fail++; printf(" FALLO %s:%d: %s == %s (%lld != %lld)\n", __FILE__, __LINE__, #a, #b, _a, _b); } } while (0)
/* Espera un estado concreto (éxito = TGB_OK). */
#define CHECK_STATUS(expr, expected) do { tgb_status _s = (expr); g_checks++; \
if (_s != (expected)) { g_fail++; printf(" FALLO %s:%d: %s devolvió %s, se esperaba %s (%s)\n", __FILE__, __LINE__, #expr, \
tgb_status_name(_s), tgb_status_name(expected), tgb_last_error()); } } while (0)
#define CHECK_OK(expr) CHECK_STATUS(expr, TGB_OK)
#define TEST(name) static void name(void)
#define RUN(name) do { int _before = g_fail; name(); printf("%s %s\n", g_fail == _before ? "[ok] " : "[FALLO]", #name); } while (0)
#define TEST_SUMMARY() do { printf("\n%d comprobaciones, %d fallos\n", g_checks, g_fail); return g_fail ? 1 : 0; } while (0)
static int _rm_cb(const char *path, const struct stat *sb, int flag, struct FTW *ftw) {
(void)sb; (void)flag; (void)ftw;
return remove(path);
}
static void tgb_test_rmtree(const char *root) { nftw(root, _rm_cb, 16, FTW_DEPTH | FTW_PHYS); }
/* Crea un contexto cuyas carpetas viven en un directorio temporal propio. */
static tgb_context *tgb_test_context(char *root, size_t cap) {
char tmpl[] = "/tmp/tgb-test-XXXXXX";
if (!mkdtemp(tmpl)) { printf("no se pudo crear carpeta temporal\n"); exit(2); }
snprintf(root, cap, "%s", tmpl);
char c[600], d[600], k[600], s[600];
snprintf(c, sizeof c, "%s/config", root);
snprintf(d, sizeof d, "%s/data", root);
snprintf(k, sizeof k, "%s/cache", root);
snprintf(s, sizeof s, "%s/state", root);
tgb_context_config cfg = {c, d, k, s};
tgb_context *ctx = NULL;
if (tgb_context_create(&cfg, &ctx) != TGB_OK) { printf("contexto: %s\n", tgb_last_error()); exit(2); }
return ctx;
}
/* Contexto sobre las MISMAS carpetas que otro (para simular otro proceso o un reinicio). */
static tgb_context *tgb_test_context_at(const char *root) {
char c[600], d[600], k[600], s[600];
snprintf(c, sizeof c, "%s/config", root);
snprintf(d, sizeof d, "%s/data", root);
snprintf(k, sizeof k, "%s/cache", root);
snprintf(s, sizeof s, "%s/state", root);
tgb_context_config cfg = {c, d, k, s};
tgb_context *ctx = NULL;
if (tgb_context_create(&cfg, &ctx) != TGB_OK) { printf("contexto: %s\n", tgb_last_error()); exit(2); }
return ctx;
}
static const tgb_color RED = {255, 0, 0, 255}, BLUE = {0, 0, 255, 255}, BLACK = {0, 0, 0, 255}, GREEN = {0, 255, 0, 255};
static size_t count_alpha(const tgb_canvas *c) {
size_t n = 0;
for (int y = 0; y < tgb_canvas_height(c); y++)
for (int x = 0; x < tgb_canvas_width(c); x++)
if (tgb_canvas_get(c, x, y).a) n++;
return n;
}
static bool is_px(const tgb_canvas *c, int x, int y, tgb_color col) {
tgb_color p = tgb_canvas_get(c, x, y);
return p.r == col.r && p.g == col.g && p.b == col.b && p.a == col.a;
}
/* Dibuja un punto en la celda (frame_index, layer_index) con sesión de edición. */
static tgb_status paint_px(tgb_document *doc, size_t f, size_t l, int x, int y, tgb_color col) {
tgb_status s = tgb_edit_begin(doc, tgb_frame_id(doc, f), tgb_layer_id(doc, l), "Punto");
if (s != TGB_OK) return s;
tgb_canvas_set(tgb_edit_canvas(doc), x, y, col);
return tgb_edit_end(doc);
}
static tgb_color px_at(const tgb_document *doc, size_t f, size_t l, int x, int y) {
return tgb_canvas_get(tgb_frame_cel(doc, f, l), x, y);
}
#endif
+113
View File
@@ -0,0 +1,113 @@
/* Implementación FALSA de las funciones de raylib que usa Tahgibu: no abre ventanas ni dibuja,
* pero reproduce un guion de ratón/teclado para ejercitar el bucle real de src/ui. */
#include <stdlib.h>
#include <string.h>
#include "raylib.h"
#include "mock_raylib.h"
static const mock_event *g_events;
static int g_nevents, g_next, g_last_frame, g_frame;
static Vector2 g_mouse;
static bool g_down[3], g_pressed[3], g_released[3];
static bool g_key_down[512], g_key_pressed[512];
static int g_chars[256], g_nchars, g_char_head;
static float g_wheel;
static int g_tex_updates, g_draw_calls;
void mock_script(const mock_event *events, int count, int last_frame) {
g_events = events; g_nevents = count; g_last_frame = last_frame; g_next = 0; g_frame = 0;
}
int mock_frames_run(void) { return g_frame; }
int mock_texture_updates(void) { return g_tex_updates; }
int mock_draw_calls(void) { return g_draw_calls; }
void SetConfigFlags(unsigned int flags) { (void)flags; }
void InitWindow(int width, int height, const char *title) { (void)width; (void)height; (void)title; }
void CloseWindow(void) {}
void SetWindowMinSize(int w, int h) { (void)w; (void)h; }
void SetTargetFPS(int fps) { (void)fps; }
void SetExitKey(int key) { (void)key; }
int GetScreenWidth(void) { return 1280; }
int GetScreenHeight(void) { return 800; }
float GetFrameTime(void) { return 1.0f / 60.0f; }
double GetTime(void) { return 1000.0 + (double)g_frame / 60.0; }
/* Se llama una vez por vuelta del bucle: avanza el guion. */
bool WindowShouldClose(void) {
g_frame++;
memset(g_pressed, 0, sizeof g_pressed);
memset(g_released, 0, sizeof g_released);
memset(g_key_pressed, 0, sizeof g_key_pressed);
g_wheel = 0;
while (g_next < g_nevents && g_events[g_next].frame <= g_frame) {
const mock_event *e = &g_events[g_next++];
int k = (int)e->a;
switch (e->type) {
case EV_MOVE: g_mouse.x = e->a; g_mouse.y = e->b; break;
case EV_DOWN: g_down[k] = true; g_pressed[k] = true; break;
case EV_UP: g_down[k] = false; g_released[k] = true; break;
case EV_KEY: if (k >= 0 && k < 512) g_key_pressed[k] = true; break;
case EV_KEYDOWN: if (k >= 0 && k < 512) g_key_down[k] = true; break;
case EV_KEYUP: if (k >= 0 && k < 512) g_key_down[k] = false; break;
case EV_CHAR: if (g_nchars < 256) g_chars[g_nchars++] = k; break;
case EV_WHEEL: g_wheel = e->a; break;
}
}
return g_frame > g_last_frame;
}
void BeginDrawing(void) {}
void EndDrawing(void) {}
void ClearBackground(Color c) { (void)c; g_draw_calls++; }
void BeginScissorMode(int x, int y, int w, int h) { (void)x; (void)y; (void)w; (void)h; }
void EndScissorMode(void) {}
void DrawRectangle(int x, int y, int w, int h, Color c) { (void)x; (void)y; (void)w; (void)h; (void)c; g_draw_calls++; }
void DrawRectangleRec(Rectangle r, Color c) { (void)r; (void)c; g_draw_calls++; }
void DrawRectangleLines(int x, int y, int w, int h, Color c) { (void)x; (void)y; (void)w; (void)h; (void)c; g_draw_calls++; }
void DrawRectangleLinesEx(Rectangle r, float t, Color c) { (void)r; (void)t; (void)c; g_draw_calls++; }
void DrawCircleLines(int x, int y, float r, Color c) { (void)x; (void)y; (void)r; (void)c; g_draw_calls++; }
void DrawText(const char *t, int x, int y, int s, Color c) { (void)t; (void)x; (void)y; (void)s; (void)c; g_draw_calls++; }
int MeasureText(const char *t, int size) { return (int)strlen(t) * size / 2; }
Font GetFontDefault(void) {
Font f;
memset(&f, 0, sizeof f);
f.baseSize = 10;
f.texture.id = 1;
f.texture.width = 1;
f.texture.height = 1;
return f;
}
Font LoadFontEx(const char *path, int size, int *codepoints, int count) {
(void)path; (void)codepoints; (void)count;
Font f = GetFontDefault();
f.baseSize = size;
f.texture.id = 2;
return f;
}
void UnloadFont(Font f) { (void)f; }
void DrawTextEx(Font f, const char *t, Vector2 p, float size, float spacing, Color c) {
(void)f; (void)t; (void)p; (void)size; (void)spacing; (void)c; g_draw_calls++;
}
Vector2 MeasureTextEx(Font f, const char *t, float size, float spacing) {
(void)f;
float n = (float)strlen(t);
return (Vector2){n * size * 0.52f + (n > 0.0f ? (n - 1.0f) * spacing : 0.0f), size};
}
Texture2D LoadTextureFromImage(Image img) { Texture2D t = {1, img.width, img.height, 1, img.format}; return t; }
void UpdateTexture(Texture2D t, const void *pixels) { (void)t; if (pixels) g_tex_updates++; }
void UnloadTexture(Texture2D t) { (void)t; }
void SetTextureFilter(Texture2D t, int f) { (void)t; (void)f; }
void DrawTexturePro(Texture2D t, Rectangle s, Rectangle d, Vector2 o, float r, Color c) { (void)t; (void)s; (void)d; (void)o; (void)r; (void)c; g_draw_calls++; }
Vector2 GetMousePosition(void) { return g_mouse; }
bool IsMouseButtonPressed(int b) { return g_pressed[b]; }
bool IsMouseButtonDown(int b) { return g_down[b]; }
bool IsMouseButtonReleased(int b) { return g_released[b]; }
float GetMouseWheelMove(void) { return g_wheel; }
bool IsKeyPressed(int k) { return k >= 0 && k < 512 && g_key_pressed[k]; }
bool IsKeyPressedRepeat(int k) { (void)k; return false; }
bool IsKeyDown(int k) { return k >= 0 && k < 512 && g_key_down[k]; }
int GetCharPressed(void) { return g_char_head < g_nchars ? g_chars[g_char_head++] : 0; }
const char *GetClipboardText(void) { return ""; }
bool CheckCollisionPointRec(Vector2 p, Rectangle r) { return p.x >= r.x && p.x < r.x + r.width && p.y >= r.y && p.y < r.y + r.height; }
+14
View File
@@ -0,0 +1,14 @@
/* Control del "raylib falso" que usa la prueba de humo de la interfaz (sin ventana ni GPU). */
#ifndef MOCK_RAYLIB_H
#define MOCK_RAYLIB_H
typedef enum { EV_MOVE, EV_DOWN, EV_UP, EV_KEY, EV_KEYDOWN, EV_KEYUP, EV_CHAR, EV_WHEEL } mock_ev_type;
typedef struct { int frame; mock_ev_type type; float a, b; } mock_event;
/* Programa un guion de eventos (ordenado por frame) y el último frame; luego tgb_ui_run lo consume. */
void mock_script(const mock_event *events, int count, int last_frame);
int mock_frames_run(void);
int mock_texture_updates(void);
int mock_draw_calls(void);
#endif
+85
View File
@@ -0,0 +1,85 @@
/* Prueba de humo de la interfaz: ejecuta el bucle REAL de src/ui con un raylib falso y un guion de
* ratón/teclado, y comprueba el resultado en el archivo .tgb guardado. NO prueba la compatibilidad
* con raylib real: solo la lógica de la interfaz y su integración con el núcleo. */
#include <stdio.h>
#include "../tgb_test.h"
#include "mock_raylib.h"
#include "../../src/ui/ui_run.h"
#include "../../src/ui/ui_app.h"
static tgb_context *g_ctx;
static char g_root[200];
static mock_event mk(int f, mock_ev_type t, float a, float b) { mock_event e = {f, t, a, b}; return e; }
#define CTRL_DOWN(f) mk(f, EV_KEYDOWN, 341, 0)
#define CTRL_UP(f) mk(f, EV_KEYUP, 341, 0)
#define KEY(f, k) mk(f, EV_KEY, (float)(k), 0)
#define MOVE(f, x, y) mk(f, EV_MOVE, x, y)
#define DOWN(f) mk(f, EV_DOWN, 0, 0)
#define UP(f) mk(f, EV_UP, 0, 0)
int main(void) {
/* El texto truncado nunca debe romper una secuencia UTF-8. */
char clipped[64];
ui_fit_text(clipped, sizeof clipped, "Rectángulo selección ñ", 100.0f, 14.0f);
CHECK(strncmp(clipped, "Rect", 4) == 0);
CHECK(strstr(clipped, "...") != NULL || strcmp(clipped, "Rectángulo selección ñ") == 0);
g_ctx = tgb_test_context(g_root, sizeof g_root);
tgb_recovery *rec = NULL;
CHECK_OK(tgb_recovery_open(g_ctx, &rec));
tgb_plugin_manager *pm = NULL;
CHECK_OK(tgb_plugin_manager_create(g_ctx, &pm));
char out[300]; snprintf(out, sizeof out, "%s/humo.tgb", g_root);
/* guion (ventana falsa de 1280x800: lienzo en x 0..980, y 104..606; "Nuevo" de la línea de tiempo en (45,625)) */
static mock_event ev[400];
int n = 0;
#define E(x) (ev[n++] = (x))
/* 1) trazo con el lápiz en el fotograma 1 */
E(MOVE(5, 300, 300)); E(DOWN(6));
for (int i = 0; i < 10; i++) E(MOVE(7 + i, 300 + 20 * (float)i, 300 + 5 * (float)i));
E(UP(18));
/* 2) deshacer y rehacer con atajos */
E(CTRL_DOWN(20)); E(KEY(20, 90)); E(CTRL_UP(21)); /* Ctrl+Z */
E(CTRL_DOWN(24)); E(KEY(24, 89)); E(CTRL_UP(25)); /* Ctrl+Y */
/* 3) rectángulo con la tecla R, arrastrando */
E(KEY(28, 82)); E(MOVE(29, 600, 200)); E(DOWN(30)); E(MOVE(31, 650, 250)); E(MOVE(32, 700, 300)); E(UP(33));
/* 4) nuevo fotograma (botón "Nuevo" de la línea de tiempo) y un trazo en él */
E(MOVE(36, 45, 625)); E(DOWN(37)); E(UP(38));
E(KEY(40, 80)); /* P: lápiz */
E(MOVE(41, 350, 350)); E(DOWN(42)); E(MOVE(43, 420, 380)); E(MOVE(44, 480, 360)); E(UP(45));
/* 5) reproducir un rato (Enter) y pausar */
E(KEY(50, 257)); E(KEY(120, 257));
/* 6) abrir "Guardar como" (Ctrl+Shift+S), borrar el texto por defecto, escribir la ruta, aceptar */
E(CTRL_DOWN(130)); E(mk(130, EV_KEYDOWN, 340, 0)); E(KEY(130, 83)); E(CTRL_UP(131)); E(mk(131, EV_KEYUP, 340, 0));
for (int i = 0; i < 40; i++) E(KEY(135 + i, 259)); /* Backspace */
for (const char *p = out; *p; p++) E(mk(180, EV_CHAR, (float)(unsigned char)*p, 0));
E(KEY(185, 257)); /* Enter */
#undef E
mock_script(ev, n, 260);
int rc = tgb_ui_run(g_ctx, rec, pm, NULL);
CHECK_EQ(rc, 0);
CHECK(mock_frames_run() > 250);
CHECK(mock_texture_updates() > 10); /* la vista se refrescó muchas veces */
CHECK(mock_draw_calls() > 5000);
/* el archivo guardado desde la interfaz contiene lo dibujado */
tgb_document *d = NULL;
CHECK_OK(tgb_document_load(g_ctx, out, &d));
if (d) {
CHECK_EQ(tgb_frame_count(d), 2);
CHECK(tgb_canvas_width(tgb_frame_cel(d, 0, 0)) == 960);
size_t f0 = count_alpha(tgb_frame_cel(d, 0, 0)), f1 = count_alpha(tgb_frame_cel(d, 1, 0));
printf(" (pixeles dibujados: fotograma 1 = %zu, fotograma 2 = %zu)\n", f0, f1);
CHECK(f0 > 150); /* trazo + rectángulo */
CHECK(f1 > 50);
CHECK(!tgb_document_dirty(d));
tgb_document_destroy(d);
}
tgb_plugin_manager_destroy(pm);
tgb_recovery_close(rec, true);
tgb_context_destroy(g_ctx);
tgb_test_rmtree(g_root);
TEST_SUMMARY();
}