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
48// an arbitrary flag word, distinctive enough that a restored row cannot carry it by accident
49#define SEEDED_FLAGS 0x51
50// likewise for a scalar column that is not the identity
51#define SEEDED_VERSION 7
52
54static void _seed_every_table(const int32_t imgid, const char *tag_name, const char *meta_value)
55{
58 /* An image imported through the application leads a group of its own;
59 * dt_image_repository_insert_import() leaves group_id NULL, and an image in no group has
60 * no membership to stage or restore -- which would make the group assertions vacuous. */
61 assert_true(dt_image_repository_set_group(imgid, imgid));
62
63 assert_true(dt_history_repository_write_item(imgid, 0, "exposure", "\x01\x02", 2, 3, TRUE,
64 "\x03", 1, 9, 0, "the instance"));
65 assert_true(dt_history_repository_write_mask_item(imgid, 0, 7, 4, "circle", 3,
66 "\xAA\xBB", 2, 2, "\x00", 1));
67 assert_true(dt_history_repository_set_module_order(imgid, 2, "exposure,colorin"));
68 assert_true(dt_history_repository_set_end(imgid, 1));
69
71
72 const guint tagid = dt_tag_repository_insert(tag_name);
73 assert_true(tagid > 0);
74 assert_true(dt_tag_repository_attach(tagid, imgid));
75
76 const dt_metadata_row_t meta = { .imgid = imgid, .keyid = 3, .value = meta_value };
78}
79
81static void _remove(const int32_t imgid)
82{
83 /* dt_grouping_remove_from_group(): a departing leader hands the group to its first
84 * survivor. A departing member changes nobody else, and this query is empty for it --
85 * nothing has a group_id of a non-leader. */
86 GList *survivors = dt_image_repository_get_group_members(imgid, imgid);
87 if(!IS_NULL_PTR(survivors))
88 dt_image_repository_reassign_group(imgid, GPOINTER_TO_INT(survivors->data), imgid);
89 g_list_free(survivors);
90
91 assert_true(dt_image_repository_delete(imgid));
92}
93
94static gboolean _in_group(const int32_t group_id, const int32_t imgid)
95{
96 GList *members = dt_image_repository_get_group_members(group_id, -1);
97 const gboolean found = !IS_NULL_PTR(g_list_find(members, GINT_TO_POINTER(imgid)));
98 g_list_free(members);
99 return found;
100}
101
102static gboolean _has_flags(const int32_t imgid, const int flags)
103{
104 GList *one = g_list_prepend(NULL, GINT_TO_POINTER(imgid));
106 const gboolean found = (g_list_length(flagged) == 1);
107 g_list_free(flagged);
108 g_list_free(one);
109 return found;
110}
111
113{
114 const int32_t film = testdb_make_film("/testdb/removal/roundtrip");
115 const int32_t imgid = testdb_make_image(film, "a.raw");
116 assert_true(film > 0 && imgid > 0);
117 _seed_every_table(imgid, "testdb|roundtrip", "a creator");
118
119 const int snap = dt_removed_image_repository_create(imgid);
120 assert_true(snap >= 0);
121 _remove(imgid);
122
123 /* The cascade really did take these, or the round trip below proves nothing. Only four
124 * child tables carry a foreign key on images(id) -- history, masks_history, tagged_images
125 * and history_hash. `module_order` and `color_labels` carry none, so their rows outlive
126 * the removal; they are staged and restored all the same, because the feature must not
127 * depend on that gap staying open, and the restore clears the survivors first so a copy
128 * on top of them cannot duplicate. */
129 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), -1);
130 assert_int_equal(dt_history_repository_count_items(imgid), 0);
131 assert_int_equal(dt_history_repository_count_mask_items(imgid), 0);
132
133 assert_true(dt_removed_image_repository_restore(snap, imgid));
134
135 // the row itself, identity and a scalar column that is not the identity
136 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), imgid);
137 assert_true(_has_flags(imgid, SEEDED_FLAGS));
138 assert_int_equal(dt_image_repository_get_version(imgid), SEEDED_VERSION);
139 assert_true(_in_group(imgid, imgid));
140
141 // and every table that hangs off it
142 assert_int_equal(dt_history_repository_count_items(imgid), 1);
143 assert_int_equal(dt_history_repository_count_mask_items(imgid), 1);
144 assert_int_equal(dt_history_repository_get_end(imgid), 1);
145
146 dt_module_order_row_t order = { 0 };
147 assert_true(dt_history_repository_get_module_order(imgid, &order));
148 assert_int_equal(order.version, 2);
149 assert_string_equal(order.iop_list, "exposure,colorin");
151
152 /* Exactly one label row, not two: main.color_labels has neither a foreign key nor a unique
153 * constraint, so the row survived the removal and an unguarded copy would double it. */
154 assert_true(dt_colorlabel_repository_has(imgid, 1));
155 GList *labels = dt_colorlabel_repository_get_list(imgid);
156 assert_int_equal(g_list_length(labels), 1);
157 g_list_free(labels);
158
159 assert_true(dt_tag_repository_is_attached(dt_tag_repository_insert("testdb|roundtrip"), imgid));
160
161 GList *values = dt_metadata_repository_get_values(imgid, 3);
162 assert_int_equal(g_list_length(values), 1);
163 assert_string_equal((const char *)values->data, "a creator");
164 g_list_free_full(values, g_free);
165}
166
168{
169 const int32_t film = testdb_make_film("/testdb/removal/group");
170 const int32_t a = testdb_make_image(film, "a.raw");
171 const int32_t b = testdb_make_image(film, "b.raw");
172 const int32_t c = testdb_make_image(film, "c.raw");
173 assert_true(a > 0 && b > 0 && c > 0);
174 assert_true(dt_image_repository_set_group(a, a));
175 assert_true(dt_image_repository_set_group(b, a));
176 assert_true(dt_image_repository_set_group(c, a));
177
178 const int snap = dt_removed_image_repository_create(a);
179 assert_true(snap >= 0);
180 _remove(a);
181
182 // the survivors were handed to a new leader -- that rewrite is what has to be undone, and
183 // it touched images nobody asked to remove
184 assert_true(_in_group(b, b));
185 assert_true(_in_group(b, c));
186
187 assert_true(dt_removed_image_repository_restore(snap, a));
188
189 assert_true(_in_group(a, a));
190 assert_true(_in_group(a, b));
191 assert_true(_in_group(a, c));
192}
193
195{
196 const int32_t film = testdb_make_film("/testdb/removal/film");
197 const int32_t imgid = testdb_make_image(film, "only.raw");
198 assert_true(film > 0 && imgid > 0);
199
200 const int snap = dt_removed_image_repository_create(imgid);
201 assert_true(snap >= 0);
202 _remove(imgid);
203
204 // dt_film_remove_empty(): the roll's last image took the roll with it
205 assert_true(dt_film_repository_delete(film));
206 assert_true(dt_film_repository_find_by_folder("/testdb/removal/film") <= 0);
207
208 assert_true(dt_removed_image_repository_restore(snap, imgid));
209
210 /* The roll must come back under its ORIGINAL id: main.images.film_id is a foreign key on
211 * it, and a roll re-created with a new id would leave the restored image pointing at
212 * nothing. The path below is a join over both tables, so it answers only if it does. */
213 assert_int_equal(dt_film_repository_find_by_folder("/testdb/removal/film"), film);
214
215 GList *ids = g_list_prepend(NULL, GINT_TO_POINTER(imgid));
216 GList *paths = dt_image_repository_get_full_paths(ids);
217 assert_int_equal(g_list_length(paths), 1);
218 assert_string_equal((const char *)paths->data, "/testdb/removal/film/only.raw");
219 g_list_free_full(paths, g_free);
220 g_list_free(ids);
221}
222
224{
225 const int32_t film = testdb_make_film("/testdb/removal/partial");
226 const int32_t a = testdb_make_image(film, "a.raw");
227 const int32_t b = testdb_make_image(film, "b.raw");
228 assert_true(dt_image_repository_set_group(a, a));
229 assert_true(dt_image_repository_set_group(b, a));
230
231 /* b leaves first, so ITS snapshot still names a as the group leader. Then a leaves too.
232 * Undoing only b is the case the restore has to survive: the group_id it wants to write
233 * back names an image that is not coming with it. */
234 const int snap_b = dt_removed_image_repository_create(b);
235 assert_true(snap_b >= 0);
236 _remove(b);
237
238 const int snap_a = dt_removed_image_repository_create(a);
239 assert_true(snap_a >= 0);
240 _remove(a);
241
242 assert_true(dt_removed_image_repository_restore(snap_b, b));
243
244 // b is back, in a group of its own rather than pointing at an image that no longer exists
245 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "b.raw"), b);
246 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), -1);
247 assert_true(_in_group(b, b));
248 assert_false(_in_group(a, b));
249}
250
252{
253 /* The batch case, which the partial one above deliberately stops short of. A whole group
254 * goes in one job, and Ctrl+Z pops every record: each restore ends by repointing at itself
255 * any image whose group_id names an image that has not come back YET, and the question is
256 * whether a later record heals that or leaves the group split for good. */
257 const int32_t film = testdb_make_film("/testdb/removal/wholegroup");
258 const int32_t a = testdb_make_image(film, "a.raw");
259 const int32_t b = testdb_make_image(film, "b.raw");
260 const int32_t c = testdb_make_image(film, "c.raw");
261 assert_true(a > 0 && b > 0 && c > 0);
262 assert_true(dt_image_repository_set_group(a, a));
263 assert_true(dt_image_repository_set_group(b, a));
264 assert_true(dt_image_repository_set_group(c, a));
265
266 // the job walks the selection in id order, snapshotting each image before it deletes it
267 const int snap_a = dt_removed_image_repository_create(a);
268 assert_true(snap_a >= 0);
269 _remove(a);
270 const int snap_b = dt_removed_image_repository_create(b);
271 assert_true(snap_b >= 0);
272 _remove(b);
273 const int snap_c = dt_removed_image_repository_create(c);
274 assert_true(snap_c >= 0);
275 _remove(c);
276
277 // one undo group, popped last-recorded-first
278 assert_true(dt_removed_image_repository_restore(snap_c, c));
279 assert_true(dt_removed_image_repository_restore(snap_b, b));
280 assert_true(dt_removed_image_repository_restore(snap_a, a));
281
282 // every image is back ...
283 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), a);
284 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "b.raw"), b);
285 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "c.raw"), c);
286
287 // ... and so is the one group they were in, under its original leader
288 assert_true(_in_group(a, a));
289 assert_true(_in_group(a, b));
290 assert_true(_in_group(a, c));
291}
292
294{
295 const int32_t film = testdb_make_film("/testdb/removal/clear");
296 const int32_t imgid = testdb_make_image(film, "a.raw");
297 _seed_every_table(imgid, "testdb|clear", "gone");
298
299 const int snap = dt_removed_image_repository_create(imgid);
300 assert_true(snap >= 0);
301 _remove(imgid);
302
303 // this is what makes a removal permanent: the undo record was discarded
305
306 /* Restoring a dropped snapshot copies nothing. It is not an error -- every statement
307 * succeeds over an empty set -- so the check is that the image stayed gone. */
308 assert_true(dt_removed_image_repository_restore(snap, imgid));
309 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), -1);
310 assert_int_equal(dt_history_repository_count_items(imgid), 0);
311}
312
314{
315 const int32_t film = testdb_make_film("/testdb/removal/twice");
316 const int32_t imgid = testdb_make_image(film, "a.raw");
317 assert_true(dt_image_repository_set_flags(imgid, SEEDED_FLAGS));
318 assert_true(dt_image_repository_set_group(imgid, imgid));
319
320 const int first = dt_removed_image_repository_create(imgid);
321 assert_true(first >= 0);
322 _remove(imgid);
323 assert_true(dt_removed_image_repository_restore(first, imgid));
324
325 /* Remove and restore the SAME image again. create() must not hand back the id the first
326 * snapshot still occupies: two copies of the row under one key would leave the primary key
327 * to decide which one the restore inserts. */
328 const int second = dt_removed_image_repository_create(imgid);
329 assert_true(second >= 0);
330 assert_int_not_equal(second, first);
331 _remove(imgid);
332 assert_true(dt_removed_image_repository_restore(second, imgid));
333
334 assert_int_equal(dt_image_repository_find_by_film_and_filename(film, "a.raw"), imgid);
335 assert_true(_has_flags(imgid, SEEDED_FLAGS));
336}
337
338int main(void)
339{
340 const struct CMUnitTest tests[] = {
341 cmocka_unit_test(test_round_trip_restores_every_table),
346 cmocka_unit_test(test_clear_drops_the_snapshot),
348 };
349 return cmocka_run_group_tests(tests, testdb_setup, testdb_teardown);
350}
#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
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
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