Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
test_removed_image_repository.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
44#include "testdb.h"
45
47#include "system/mem_alloc.h"
48
49#ifdef _WIN32
50#include "win/main_wrapper.h"
51#endif
52
53// an arbitrary flag word, distinctive enough that a restored row cannot carry it by accident
54#define SEEDED_FLAGS 0x51
55// likewise for a scalar column that is not the identity
56#define SEEDED_VERSION 7
57
59static void _seed_every_table(const int32_t imgid, const char *tag_name, const char *meta_value)
60{
63 /* An image imported through the application leads a group of its own;
64 * dt_image_repository_insert_import() leaves group_id NULL, and an image in no group has
65 * no membership to stage or restore -- which would make the group assertions vacuous. */
66 assert_true(dt_image_repository_set_group(imgid, imgid));
67
68 assert_true(dt_history_repository_write_item(imgid, 0, "exposure", "\x01\x02", 2, 3, TRUE,
69 "\x03", 1, 9, 0, "the instance"));
70 assert_true(dt_history_repository_write_mask_item(imgid, 0, 7, 4, "circle", 3,
71 "\xAA\xBB", 2, 2, "\x00", 1));
72 assert_true(dt_history_repository_set_module_order(imgid, 2, "exposure,colorin"));
73 assert_true(dt_history_repository_set_end(imgid, 1));
74
76
77 const guint tagid = dt_tag_repository_insert(tag_name);
78 assert_true(tagid > 0);
79 assert_true(dt_tag_repository_attach(tagid, imgid));
80
81 const dt_metadata_row_t meta = { .imgid = imgid, .keyid = 3, .value = meta_value };
83}
84
86static void _remove(const int32_t imgid)
87{
88 /* dt_grouping_remove_from_group(): a departing leader hands the group to its first
89 * survivor. A departing member changes nobody else, and this query is empty for it --
90 * nothing has a group_id of a non-leader. */
91 GList *survivors = dt_image_repository_get_group_members(imgid, imgid);
92 if(!IS_NULL_PTR(survivors))
93 dt_image_repository_reassign_group(imgid, GPOINTER_TO_INT(survivors->data), imgid);
94 g_list_free(survivors);
95
96 assert_true(dt_image_repository_delete(imgid));
97}
98
99static gboolean _in_group(const int32_t group_id, const int32_t imgid)
100{
101 GList *members = dt_image_repository_get_group_members(group_id, -1);
102 const gboolean found = !IS_NULL_PTR(g_list_find(members, GINT_TO_POINTER(imgid)));
103 g_list_free(members);
104 return found;
105}
106
107static gboolean _has_flags(const int32_t imgid, const int flags)
108{
109 GList *one = g_list_prepend(NULL, GINT_TO_POINTER(imgid));
111 const gboolean found = (g_list_length(flagged) == 1);
112 g_list_free(flagged);
113 g_list_free(one);
114 return found;
115}
116
118{
119 const int32_t film = testdb_make_film("/testdb/removal/roundtrip");
120 const int32_t imgid = testdb_make_image(film, "a.raw");
121 assert_true(film > 0 && imgid > 0);
122 _seed_every_table(imgid, "testdb|roundtrip", "a creator");
123
124 const int snap = dt_removed_image_repository_create(imgid);
125 assert_true(snap >= 0);
126 _remove(imgid);
127
128 /* The cascade really did take these, or the round trip below proves nothing. Only four
129 * child tables carry a foreign key on images(id) -- history, masks_history, tagged_images
130 * and history_hash. `module_order` and `color_labels` carry none, so their rows outlive
131 * the removal; they are staged and restored all the same, because the feature must not
132 * depend on that gap staying open, and the restore clears the survivors first so a copy
133 * on top of them cannot duplicate. */
134 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), -1);
135 assert_int_equal(dt_history_repository_count_items(imgid), 0);
136 assert_int_equal(dt_history_repository_count_mask_items(imgid), 0);
137
138 assert_true(dt_removed_image_repository_restore(snap, imgid));
139
140 // the row itself, identity and a scalar column that is not the identity
141 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), imgid);
142 assert_true(_has_flags(imgid, SEEDED_FLAGS));
143 assert_int_equal(dt_image_repository_get_version(imgid), SEEDED_VERSION);
144 assert_true(_in_group(imgid, imgid));
145
146 // and every table that hangs off it
147 assert_int_equal(dt_history_repository_count_items(imgid), 1);
148 assert_int_equal(dt_history_repository_count_mask_items(imgid), 1);
149 assert_int_equal(dt_history_repository_get_end(imgid), 1);
150
151 dt_module_order_row_t order = { 0 };
152 assert_true(dt_history_repository_get_module_order(imgid, &order));
153 assert_int_equal(order.version, 2);
154 assert_string_equal(order.iop_list, "exposure,colorin");
156
157 /* Exactly one label row, not two: main.color_labels has neither a foreign key nor a unique
158 * constraint, so the row survived the removal and an unguarded copy would double it. */
159 assert_true(dt_colorlabel_repository_has(imgid, 1));
160 GList *labels = dt_colorlabel_repository_get_list(imgid);
161 assert_int_equal(g_list_length(labels), 1);
162 g_list_free(labels);
163
164 assert_true(dt_tag_repository_is_attached(dt_tag_repository_insert("testdb|roundtrip"), imgid));
165
166 GList *values = dt_metadata_repository_get_values(imgid, 3);
167 assert_int_equal(g_list_length(values), 1);
168 assert_string_equal((const char *)values->data, "a creator");
169 g_list_free_full(values, g_free);
170}
171
173{
174 const int32_t film = testdb_make_film("/testdb/removal/group");
175 const int32_t a = testdb_make_image(film, "a.raw");
176 const int32_t b = testdb_make_image(film, "b.raw");
177 const int32_t c = testdb_make_image(film, "c.raw");
178 assert_true(a > 0 && b > 0 && c > 0);
179 assert_true(dt_image_repository_set_group(a, a));
180 assert_true(dt_image_repository_set_group(b, a));
181 assert_true(dt_image_repository_set_group(c, a));
182
183 const int snap = dt_removed_image_repository_create(a);
184 assert_true(snap >= 0);
185 _remove(a);
186
187 // the survivors were handed to a new leader -- that rewrite is what has to be undone, and
188 // it touched images nobody asked to remove
189 assert_true(_in_group(b, b));
190 assert_true(_in_group(b, c));
191
192 assert_true(dt_removed_image_repository_restore(snap, a));
193
194 assert_true(_in_group(a, a));
195 assert_true(_in_group(a, b));
196 assert_true(_in_group(a, c));
197}
198
200{
201 const int32_t film = testdb_make_film("/testdb/removal/film");
202 const int32_t imgid = testdb_make_image(film, "only.raw");
203 assert_true(film > 0 && imgid > 0);
204
205 const int snap = dt_removed_image_repository_create(imgid);
206 assert_true(snap >= 0);
207 _remove(imgid);
208
209 // dt_film_remove_empty(): the roll's last image took the roll with it
210 assert_true(dt_film_repository_delete(film));
211 assert_true(dt_film_repository_find_by_folder("/testdb/removal/film") <= 0);
212
213 assert_true(dt_removed_image_repository_restore(snap, imgid));
214
215 /* The roll must come back under its ORIGINAL id: main.images.film_id is a foreign key on
216 * it, and a roll re-created with a new id would leave the restored image pointing at
217 * nothing. The path below is a join over both tables, so it answers only if it does. */
218 assert_int_equal(dt_film_repository_find_by_folder("/testdb/removal/film"), film);
219
220 GList *ids = g_list_prepend(NULL, GINT_TO_POINTER(imgid));
221 GList *paths = dt_image_repository_get_full_paths(ids);
222 assert_int_equal(g_list_length(paths), 1);
223 // the repository joins folder and file with the platform's separator, whatever the folder uses
224 gchar *expected = g_strconcat("/testdb/removal/film", G_DIR_SEPARATOR_S, "only.raw", NULL);
225 assert_string_equal((const char *)paths->data, expected);
226 dt_free(expected);
227 g_list_free_full(paths, g_free);
228 g_list_free(ids);
229}
230
232{
233 const int32_t film = testdb_make_film("/testdb/removal/partial");
234 const int32_t a = testdb_make_image(film, "a.raw");
235 const int32_t b = testdb_make_image(film, "b.raw");
236 assert_true(dt_image_repository_set_group(a, a));
237 assert_true(dt_image_repository_set_group(b, a));
238
239 /* b leaves first, so ITS snapshot still names a as the group leader. Then a leaves too.
240 * Undoing only b is the case the restore has to survive: the group_id it wants to write
241 * back names an image that is not coming with it. */
242 const int snap_b = dt_removed_image_repository_create(b);
243 assert_true(snap_b >= 0);
244 _remove(b);
245
246 const int snap_a = dt_removed_image_repository_create(a);
247 assert_true(snap_a >= 0);
248 _remove(a);
249
250 assert_true(dt_removed_image_repository_restore(snap_b, b));
251
252 // b is back, in a group of its own rather than pointing at an image that no longer exists
253 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "b.raw"), b);
254 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), -1);
255 assert_true(_in_group(b, b));
256 assert_false(_in_group(a, b));
257}
258
260{
261 /* The batch case, which the partial one above deliberately stops short of. A whole group
262 * goes in one job, and Ctrl+Z pops every record: each restore ends by repointing at itself
263 * any image whose group_id names an image that has not come back YET, and the question is
264 * whether a later record heals that or leaves the group split for good. */
265 const int32_t film = testdb_make_film("/testdb/removal/wholegroup");
266 const int32_t a = testdb_make_image(film, "a.raw");
267 const int32_t b = testdb_make_image(film, "b.raw");
268 const int32_t c = testdb_make_image(film, "c.raw");
269 assert_true(a > 0 && b > 0 && c > 0);
270 assert_true(dt_image_repository_set_group(a, a));
271 assert_true(dt_image_repository_set_group(b, a));
272 assert_true(dt_image_repository_set_group(c, a));
273
274 // the job walks the selection in id order, snapshotting each image before it deletes it
275 const int snap_a = dt_removed_image_repository_create(a);
276 assert_true(snap_a >= 0);
277 _remove(a);
278 const int snap_b = dt_removed_image_repository_create(b);
279 assert_true(snap_b >= 0);
280 _remove(b);
281 const int snap_c = dt_removed_image_repository_create(c);
282 assert_true(snap_c >= 0);
283 _remove(c);
284
285 // one undo group, popped last-recorded-first
286 assert_true(dt_removed_image_repository_restore(snap_c, c));
287 assert_true(dt_removed_image_repository_restore(snap_b, b));
288 assert_true(dt_removed_image_repository_restore(snap_a, a));
289
290 // every image is back ...
291 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), a);
292 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "b.raw"), b);
293 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "c.raw"), c);
294
295 // ... and so is the one group they were in, under its original leader
296 assert_true(_in_group(a, a));
297 assert_true(_in_group(a, b));
298 assert_true(_in_group(a, c));
299}
300
302{
303 const int32_t film = testdb_make_film("/testdb/removal/clear");
304 const int32_t imgid = testdb_make_image(film, "a.raw");
305 _seed_every_table(imgid, "testdb|clear", "gone");
306
307 const int snap = dt_removed_image_repository_create(imgid);
308 assert_true(snap >= 0);
309 _remove(imgid);
310
311 // this is what makes a removal permanent: the undo record was discarded
313
314 /* Restoring a dropped snapshot copies nothing. It is not an error -- every statement
315 * succeeds over an empty set -- so the check is that the image stayed gone. */
316 assert_true(dt_removed_image_repository_restore(snap, imgid));
317 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), -1);
318 assert_int_equal(dt_history_repository_count_items(imgid), 0);
319}
320
322{
323 const int32_t film = testdb_make_film("/testdb/removal/twice");
324 const int32_t imgid = testdb_make_image(film, "a.raw");
325 assert_true(dt_image_repository_set_flags(imgid, SEEDED_FLAGS));
326 assert_true(dt_image_repository_set_group(imgid, imgid));
327
328 const int first = dt_removed_image_repository_create(imgid);
329 assert_true(first >= 0);
330 _remove(imgid);
331 assert_true(dt_removed_image_repository_restore(first, imgid));
332
333 /* Remove and restore the SAME image again. create() must not hand back the id the first
334 * snapshot still occupies: two copies of the row under one key would leave the primary key
335 * to decide which one the restore inserts. */
336 const int second = dt_removed_image_repository_create(imgid);
337 assert_true(second >= 0);
338 assert_int_not_equal(second, first);
339 _remove(imgid);
340 assert_true(dt_removed_image_repository_restore(second, imgid));
341
342 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), imgid);
343 assert_true(_has_flags(imgid, SEEDED_FLAGS));
344}
345
346int main(int argc, char *argv[])
347{
348 const struct CMUnitTest tests[] = {
349 cmocka_unit_test(test_round_trip_restores_every_table),
354 cmocka_unit_test(test_clear_drops_the_snapshot),
356 };
357 return cmocka_run_group_tests(tests, testdb_setup, testdb_teardown);
358}
#define TRUE
Definition ashift_lsd.c:162
gboolean dt_colorlabel_repository_has(const int32_t imgid, const int color)
TRUE when colour color is set on imgid.
void dt_colorlabel_repository_set(const int32_t imgid, const int color)
Set colour color on imgid. Setting one that is already set does nothing.
GList * dt_colorlabel_repository_get_list(const int32_t imgid)
The colours set on imgid, as a list rather than a bitmask.
int32_t dt_film_repository_find_by_folder(const char *folder)
gboolean dt_film_repository_delete(const int32_t id)
gboolean dt_history_repository_get_module_order(const int32_t imgid, dt_module_order_row_t *row)
Read imgid's row. FALSE when there is none, and row is then left zeroed.
gboolean dt_history_repository_write_item(const int32_t imgid, const int num, const char *operation, const void *op_params, const int op_params_size, const int module_version, const gboolean enabled, const void *blendop_params, const int blendop_params_size, const int blendop_version, const int multi_priority, const char *multi_name)
gboolean dt_history_repository_write_mask_item(const int32_t imgid, const int num, const int formid, const int form, const char *name, const int version, const void *points, const int points_len, const int points_count, const void *source, const int source_len)
int dt_history_repository_count_mask_items(const int32_t imgid)
void dt_module_order_row_cleanup(dt_module_order_row_t *row)
Release what dt_history_repository_get_module_order() allocated.
gboolean dt_history_repository_set_module_order(const int32_t imgid, const int version, const char *iop_list)
Store imgid's order: version, and iop_list serialized or NULL.
int dt_history_repository_count_items(const int32_t imgid)
gboolean dt_history_repository_set_end(const int32_t imgid, const int32_t history_end)
int32_t dt_history_repository_get_end(const int32_t imgid)
int dt_image_repository_get_version(const int32_t imgid)
The duplicate version number of imgid, or 0.
GList * dt_image_repository_get_ids_with_flag_among(GList *imgids, const int flag)
Of the images in imgids, those whose flags carry flag, in row order.
gboolean dt_image_repository_set_group(const int32_t imgid, const int32_t group_id)
Set imgid's group_id.
gboolean dt_image_repository_delete(const int32_t imgid)
Delete imgid from main.images and main.meta_data.
gboolean dt_image_repository_set_flags(const int32_t imgid, const int flags)
Replace imgid's whole flags word.
void dt_image_repository_reassign_group(const int32_t from_group_id, const int32_t to_group_id, const int32_t exclude_imgid)
Move every member of from_group_id into to_group_id, except exclude_imgid.
int32_t dt_image_repository_find_by_film_and_filename(const int32_t film_id, const char *filename)
The image id for filename inside film roll film_id, or -1.
gboolean dt_image_repository_set_version(const int32_t imgid, const int version)
Set both version and max_version of imgid to version.
GList * dt_image_repository_get_group_members(const int32_t group_id, const int32_t exclude_imgid)
Image ids in group group_id.
GList * dt_image_repository_get_full_paths(GList *imgids)
"<folder>/<filename>" for every image in imgids, distinct, in row order. Caller owns the strings.
static CameraMetaData * meta
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition macros.h:96
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
GList * dt_metadata_repository_get_values(const int32_t imgid, const int keyid)
Values stored under keyid, ordered by value.
void dt_metadata_repository_add(const dt_metadata_row_t *rows, const size_t count)
Insert count rows as a single multi-VALUES statement.
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
int main()
Definition prova.c:47
void dt_removed_image_repository_clear(const int snap_id, const int32_t imgid)
Drop snapshot snap_id of imgid, making the removal permanent.
gboolean dt_removed_image_repository_restore(const int snap_id, const int32_t imgid)
Copy snapshot snap_id of imgid back into main, film roll included.
int dt_removed_image_repository_create(const int32_t imgid)
Copy every row imgid owns out of main, so the removal about to happen can be undone.
memory.removed_*: the staging area behind "undo remove from library".
const float uint32_t state[4]
One main.module_order row.
gboolean dt_tag_repository_attach(const guint tagid, const int32_t imgid)
Attach tagid to imgid, at the end of the tag order.
gboolean dt_tag_repository_is_attached(const guint tagid, const int32_t imgid)
TRUE when tagid is attached to imgid.
guint dt_tag_repository_insert(const char *name)
Insert a tag named name and return its new id, or 0 on failure.
#define SEEDED_FLAGS
static void test_group_leader_restores_membership(void **state)
static void test_partial_undo_repoints_a_departed_leader(void **state)
static void test_whole_group_removed_and_fully_undone_keeps_one_group(void **state)
static void test_clear_drops_the_snapshot(void **state)
static gboolean _has_flags(const int32_t imgid, const int flags)
static gboolean _in_group(const int32_t group_id, const int32_t imgid)
static void test_film_roll_comes_back_with_its_last_image(void **state)
static void _remove(const int32_t imgid)
static void _seed_every_table(const int32_t imgid, const char *tag_name, const char *meta_value)
#define SEEDED_VERSION
static void test_round_trip_restores_every_table(void **state)
static void test_successive_snapshots_do_not_collide(void **state)
Shared fixture for the src/database repository tests.
static int testdb_setup(void **state)
Definition testdb.h:55
static int32_t testdb_make_film(const char *folder)
Definition testdb.h:85
static int testdb_teardown(void **state)
Definition testdb.h:66
static int32_t testdb_make_image(const int32_t film_id, const char *filename)
Definition testdb.h:92