/* Modelo de documento, sesiones de edición e historial. */ #include #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(); }