308 lines
15 KiB
C
308 lines
15 KiB
C
/* 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();
|
|
}
|