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

324 lines
16 KiB
C

/* 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();
}