/* Autoguardado cada 5 minutos y recuperación tras cierres inesperados (con procesos reales). */ #include #include #include #include #include #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(); }