Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
tests/styles.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2026 Guillaume Stutin.
4
5 darktable 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 darktable 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 darktable. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19#include "darktable.h"
20#include "database/database.h"
21#include "common/film.h"
23#include "common/image.h"
24#include "common/styles.h"
25
26#include <assert.h>
27#include <gio/gio.h>
28#include <glib/gstdio.h>
29#include <sqlite3.h>
30#include <stdio.h>
31#include <stdlib.h>
32
33#ifdef _WIN32
34#include "win/main_wrapper.h"
35#endif
36
37#ifndef ANSEL_TEST_SOURCE_DIR
38#define ANSEL_TEST_SOURCE_DIR "."
39#endif
40
41#ifndef ANSEL_TEST_BINARY_DIR
42#define ANSEL_TEST_BINARY_DIR "."
43#endif
44
59static char *test_image_dir = NULL;
60
74
75static int test_fail(const char *scenario, const char *message, char **failure_reason)
76{
77 fprintf(stderr, "[FAIL] %s: %s\n", scenario, message);
79 *failure_reason = g_strdup(message);
80 return 1;
81}
82
83static gboolean is_style_file(const char *filename)
84{
85 const char *extension = strrchr(filename, '.');
86 return !IS_NULL_PTR(extension) && !g_ascii_strcasecmp(extension, ".dtstyle");
87}
88
89static char *scenario_name_from_style_file(const char *filename)
90{
91 char *scenario = g_strdup(filename);
92 char *extension = strrchr(scenario, '.');
93 if(!IS_NULL_PTR(extension)) *extension = '\0';
94 return scenario;
95}
96
97static int sql_int_for_bound_images(const char *query, const int32_t imgid_a, const int32_t imgid_b)
98{
103 const int value = (sqlite3_step(stmt) == SQLITE_ROW) ? sqlite3_column_int(stmt, 0) : -1;
105 return value;
106}
107
108static int max_style_id(void)
109{
111 sqlite3_prepare_v2(dt_database_get_sqlite3_global(), "SELECT COALESCE(MAX(id), 0) FROM data.styles", -1,
112 &stmt, NULL);
113 const int value = (sqlite3_step(stmt) == SQLITE_ROW) ? sqlite3_column_int(stmt, 0) : 0;
115 return value;
116}
117
119{
122 "SELECT name FROM data.styles WHERE id > ?1 ORDER BY id DESC LIMIT 1", -1, &stmt,
123 NULL);
125
126 char *name = NULL;
128 name = g_strdup((const char *)sqlite3_column_text(stmt, 0));
129
131 return name;
132}
133
134static int32_t create_test_image(const char *source_image_path)
135{
136 static int image_index = 0;
137 const char *extension = strrchr(source_image_path, '.');
138 char *filename = g_strdup_printf("style-test-%d%s", image_index++, extension ? extension : ".raw");
139 char *image_path = g_build_filename(test_image_dir, filename, NULL);
140
142 GFile *image_file = g_file_new_for_path(image_path);
143 GError *error = NULL;
147
148 if(!copied)
149 {
150 fprintf(stderr, "[FAIL] copy test image: %s\n", error ? error->message : "unknown error");
152 dt_free(filename);
153 dt_free(image_path);
154 return 0;
155 }
156
157 dt_film_t film;
158 dt_film_init(&film);
159 if(dt_film_new(&film, test_image_dir) <= 0)
160 {
161 dt_film_cleanup(&film);
162 dt_free(filename);
163 dt_free(image_path);
164 return 0;
165 }
166
167 const int32_t imgid = dt_image_import(film.id, image_path, FALSE);
168
169 dt_film_cleanup(&film);
170 dt_free(filename);
171 dt_free(image_path);
172 return imgid;
173}
174
175static int load_xmp_on_image(const char *scenario, const int32_t imgid, const char *xmp_path,
176 char **failure_reason)
177{
178 if(!g_file_test(xmp_path, G_FILE_TEST_IS_REGULAR))
179 return test_fail(scenario, "missing XMP fixture", failure_reason);
180
181 return dt_history_load_and_apply(imgid, (gchar *)xmp_path, TRUE)
182 ? test_fail(scenario, "could not load XMP fixture", failure_reason)
183 : 0;
184}
185
186static int load_start_history(const char *scenario, const int32_t imgid, const char *start_xmp_path,
187 const char *start_xmp_name, char **failure_reason)
188{
191
192 printf("[NOTE] %s: no %s fixture, using imported base history\n", scenario, start_xmp_name);
193 return 0;
194}
195
196static void print_module_order_summary(const char *label, const int32_t imgid)
197{
200 "SELECT version, iop_list FROM main.module_order WHERE imgid=?1", -1, &stmt, NULL);
201 sqlite3_bind_int(stmt, 1, imgid);
202
204 {
205 const unsigned char *iop_list = sqlite3_column_text(stmt, 1);
206 fprintf(stderr, " %s pipe order for image %d:\n", label, imgid);
207 fprintf(stderr, " version=%d\n", sqlite3_column_int(stmt, 0));
208 fprintf(stderr, " iop_list=%s\n", iop_list ? (const char *)iop_list : "(null)");
209 }
210 else
211 {
212 fprintf(stderr, " %s pipe order for image %d: no module_order row\n", label, imgid);
213 }
214
216}
217
218static int compare_pipe_order(const char *scenario, const int32_t actual_imgid, const int32_t expected_imgid,
219 char **failure_reason)
220{
221 const int diff = sql_int_for_bound_images(
222 "SELECT"
223 " (SELECT COUNT(*) FROM"
224 " (SELECT version, iop_list FROM main.module_order WHERE imgid=?1"
225 " EXCEPT"
226 " SELECT version, iop_list FROM main.module_order WHERE imgid=?2))"
227 " +"
228 " (SELECT COUNT(*) FROM"
229 " (SELECT version, iop_list FROM main.module_order WHERE imgid=?2"
230 " EXCEPT"
231 " SELECT version, iop_list FROM main.module_order WHERE imgid=?1))",
233
234 if(!diff) return 0;
235
236 test_fail(scenario, "module order differs from expected XMP", failure_reason);
239 return 1;
240}
241
242static void print_enabled_state_summary(const char *label, const int32_t imgid)
243{
244 fprintf(stderr, " %s enabled state for image %d:\n", label, imgid);
245
249 "SELECT h.operation, h.multi_priority, IFNULL(h.multi_name, ''), h.enabled"
250 " FROM main.history h"
251 " WHERE h.imgid=?1"
252 " AND h.num < (SELECT history_end FROM main.images WHERE id=?1)"
253 " AND h.num = (SELECT MAX(h2.num)"
254 " FROM main.history h2"
255 " WHERE h2.imgid=?1"
256 " AND h2.operation=h.operation"
257 " AND h2.multi_priority=h.multi_priority"
258 " AND h2.num < (SELECT history_end FROM main.images WHERE id=?1))"
259 " ORDER BY h.operation, h.multi_priority",
260 -1, &stmt, NULL);
261 sqlite3_bind_int(stmt, 1, imgid);
262
263 while(sqlite3_step(stmt) == SQLITE_ROW)
264 {
265 const unsigned char *operation = sqlite3_column_text(stmt, 0);
266 const unsigned char *multi_name = sqlite3_column_text(stmt, 2);
267 fprintf(stderr, " %-24s priority=%d name='%s' enabled=%d\n",
268 operation ? (const char *)operation : "", sqlite3_column_int(stmt, 1),
269 multi_name ? (const char *)multi_name : "", sqlite3_column_int(stmt, 3));
270 }
271
273}
274
275static int compare_enabled_state(const char *scenario, const int32_t actual_imgid,
276 const int32_t expected_imgid, char **failure_reason)
277{
278 const int diff = sql_int_for_bound_images(
279 "WITH actual AS ("
280 " SELECT h.operation, h.multi_priority, IFNULL(h.multi_name, '') AS multi_name, h.enabled"
281 " FROM main.history h"
282 " WHERE h.imgid=?1"
283 " AND h.num < (SELECT history_end FROM main.images WHERE id=?1)"
284 " AND h.num = (SELECT MAX(h2.num)"
285 " FROM main.history h2"
286 " WHERE h2.imgid=?1"
287 " AND h2.operation=h.operation"
288 " AND h2.multi_priority=h.multi_priority"
289 " AND h2.num < (SELECT history_end FROM main.images WHERE id=?1))"
290 "), expected AS ("
291 " SELECT h.operation, h.multi_priority, IFNULL(h.multi_name, '') AS multi_name, h.enabled"
292 " FROM main.history h"
293 " WHERE h.imgid=?2"
294 " AND h.num < (SELECT history_end FROM main.images WHERE id=?2)"
295 " AND h.num = (SELECT MAX(h2.num)"
296 " FROM main.history h2"
297 " WHERE h2.imgid=?2"
298 " AND h2.operation=h.operation"
299 " AND h2.multi_priority=h.multi_priority"
300 " AND h2.num < (SELECT history_end FROM main.images WHERE id=?2))"
301 ")"
302 "SELECT"
303 " (SELECT COUNT(*) FROM (SELECT * FROM actual EXCEPT SELECT * FROM expected))"
304 " +"
305 " (SELECT COUNT(*) FROM (SELECT * FROM expected EXCEPT SELECT * FROM actual))",
307
308 if(!diff) return 0;
309
310 test_fail(scenario, "final enabled states differ from expected XMP", failure_reason);
313 return 1;
314}
315
316static int apply_style_to_image(const char *scenario, const char *style_name, const int32_t imgid,
317 char **failure_reason)
318{
319 const int style_id = dt_styles_get_id_by_name(style_name);
320 if(style_id == 0)
321 return test_fail(scenario, "imported style is missing from the database", failure_reason);
322
323 dt_conf_set_bool("history/copy_iop_order", FALSE);
324 dt_conf_set_bool("history/paste_instances", TRUE);
325
327 ? test_fail(scenario, "style application failed", failure_reason)
328 : 0;
329}
330
331static int run_style_scenario(const char *scenario_dir, const char *source_image_path, const char *style_file,
332 char **failure_reason)
333{
334 char *scenario = scenario_name_from_style_file(style_file);
335 char *style_path = g_build_filename(scenario_dir, style_file, NULL);
336 char *start_xmp_name = g_strdup_printf("start_%s.xmp", scenario);
337 char *end_xmp_name = g_strdup_printf("end_%s.xmp", scenario);
340
341 printf("\n[STEP] %s\n", scenario);
342
343 int result = 1;
348
351
352 if(actual_imgid <= 0 || expected_imgid <= 0)
353 {
354 test_fail(scenario, "could not import test image", failure_reason);
355 goto end;
356 }
357
363
364 printf("[OK] %s\n", scenario);
365 result = 0;
366
367end:
375 return result;
376}
377
378static int run_style_scenarios(const char *scenario_dir, const char *source_image_path)
379{
380 GError *error = NULL;
381 GDir *dir = g_dir_open(scenario_dir, 0, &error);
382 if(IS_NULL_PTR(dir))
383 {
384 fprintf(stderr, "[FAIL] cannot open style scenario folder `%s`: %s\n", scenario_dir,
385 error ? error->message : "unknown error");
387 return 1;
388 }
389
391 const char *entry = NULL;
392 while((entry = g_dir_read_name(dir)) != NULL)
393 {
395 }
396 g_dir_close(dir);
397
400 {
401 printf("[SKIP] no .dtstyle file found in %s\n", scenario_dir);
402 return 0;
403 }
404
405 int result = 0;
407 for(GList *l = style_files; l; l = g_list_next(l))
408 {
409 const char *style_file = (const char *)l->data;
410 dt_style_scenario_result_t *summary = g_malloc0(sizeof(*summary));
411 summary->style_file = g_strdup(style_file);
412 summary->result = run_style_scenario(scenario_dir, source_image_path, style_file, &summary->reason);
413 if(summary->result && IS_NULL_PTR(summary->reason))
414 summary->reason = g_strdup("unknown failure");
415
417 result |= summary->result;
418 }
419
421 printf("\n[SUMMARY]\n");
422 for(GList *l = summaries; l; l = g_list_next(l))
423 {
425 printf("%s: %s", summary->style_file, summary->result ? "FAILED" : "PASSED");
426 if(summary->result)
427 printf(" - %s", summary->reason);
428 printf("\n");
429 }
430
431 for(GList *l = summaries; l; l = g_list_next(l))
432 {
434 dt_free(summary->style_file);
435 dt_free(summary->reason);
436 dt_free(summary);
437 }
440 return result;
441}
442
443static char *prepare_test_datadir(const char *tmp_dir)
444{
445 char *datadir = g_build_filename(tmp_dir, "data", NULL);
446 char *rawspeed_dir = g_build_filename(datadir, "rawspeed", NULL);
447 char *rawspeed_xml = g_build_filename(rawspeed_dir, "cameras.xml", NULL);
448 char *rawspeed_source_xml = g_build_filename(ANSEL_TEST_SOURCE_DIR, "src", "external", "rawspeed", "data",
449 "cameras.xml", NULL);
450
451 g_mkdir(datadir, 0700);
452 g_mkdir(rawspeed_dir, 0700);
453
454 GError *error = NULL;
460
461 if(!copied)
462 {
463 fprintf(stderr, "[FAIL] copy rawspeed cameras.xml: %s\n", error ? error->message : "unknown error");
465 dt_free(datadir);
466 datadir = NULL;
467 }
468
472 return datadir;
473}
474
475int main(int argc, char *argv[])
476{
477 const gboolean explicit_scenario_dir = argc > 1;
479 char *default_source_image = g_build_filename(ANSEL_TEST_SOURCE_DIR, "tests", "integration", "images",
480 "mire1.cr2", NULL);
482 const char *source_image_path = (argc > 2) ? argv[2] : default_source_image;
483
485 {
486 fprintf(stderr, explicit_scenario_dir ? "[FAIL] style scenario folder does not exist: %s\n"
487 : "[SKIP] style scenario folder does not exist: %s\n",
491 return explicit_scenario_dir ? 1 : 0;
492 }
493
495 {
496 fprintf(stderr, "[FAIL] test image does not exist: %s\n", source_image_path);
499 return 1;
500 }
501
502 char *config_dir = g_strdup_printf("%s/ansel-test-styles-config-XXXXXX", g_get_tmp_dir());
503 char *cache_dir = g_strdup_printf("%s/ansel-test-styles-cache-XXXXXX", g_get_tmp_dir());
504 char *tmp_dir = g_strdup_printf("%s/ansel-test-styles-tmp-XXXXXX", g_get_tmp_dir());
505
510
513 {
519 return 1;
520 }
521
522 char *noiseprofiles = g_build_filename(ANSEL_TEST_SOURCE_DIR, "data", "noiseprofiles.json", NULL);
523 char *argv_override[] = {
524 "ansel-test-styles",
525 "--library", ":memory:",
526 "--datadir", test_datadir,
527 "--noiseprofiles", noiseprofiles,
528 "--moduledir", ANSEL_TEST_BINARY_DIR "/lib/ansel",
529 "--configdir", config_dir,
530 "--cachedir", cache_dir,
531 "--tmpdir", tmp_dir,
532 "--disable-opencl",
533 "--conf", "write_sidecar_files=FALSE",
534 "--conf", "plugins/lighttable/export/force_lcms2=FALSE",
535 "-t", "1",
536 NULL
537 };
538 int argc_override = sizeof(argv_override) / sizeof(*argv_override) - 1;
539
541
543
544 dt_cleanup();
552
553 return result;
554}
555
556// clang-format off
557// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
558// vim: shiftwidth=2 expandtab tabstop=2 cindent
559// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
560// clang-format on
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
const char * extension(dt_imageio_module_data_t *data)
Definition avif.c:651
void dt_conf_set_bool(const char *name, int val)
int32_t dt_image_import(const int32_t film_id, const char *filename, gboolean raise_signals)
void dt_cleanup()
Definition darktable.c:1864
int dt_init(int argc, char *argv[], const gboolean init_gui, const gboolean load_data)
Definition darktable.c:885
sqlite3 * dt_database_get_sqlite3_global(void)
Definition database.c:3790
void dt_film_init(dt_film_t *film)
Definition film.c:74
int dt_film_new(dt_film_t *film, const char *directory)
Definition film.c:129
void dt_film_cleanup(dt_film_t *film)
Definition film.c:84
int dt_history_load_and_apply(const int32_t imgid, gchar *filename, int history_only)
@ DT_HISTORY_MERGE_APPEND
#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
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
const char * name
Definition pdf.h:90
int main()
Definition prova.c:47
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
int dt_styles_apply_to_image_merge(const char *name, const int style_id, const int32_t newimgid, const dt_history_merge_strategy_t mode, dt_hm_batch_state_t *batch)
int32_t dt_styles_get_id_by_name(const char *name)
void dt_styles_import_from_file(const char *style_path)
int32_t id
Definition film.h:45
One-line scenario result printed after every style has run.
static int test_fail(const char *scenario, const char *message, char **failure_reason)
static int sql_int_for_bound_images(const char *query, const int32_t imgid_a, const int32_t imgid_b)
static char * prepare_test_datadir(const char *tmp_dir)
static int load_start_history(const char *scenario, const int32_t imgid, const char *start_xmp_path, const char *start_xmp_name, char **failure_reason)
static char * imported_style_name(const int before_import_style_id)
static char * test_image_dir
Run style-application scenarios from tests/styles.
static int compare_enabled_state(const char *scenario, const int32_t actual_imgid, const int32_t expected_imgid, char **failure_reason)
static int run_style_scenarios(const char *scenario_dir, const char *source_image_path)
#define ANSEL_TEST_SOURCE_DIR
static int32_t create_test_image(const char *source_image_path)
static int max_style_id(void)
static char * scenario_name_from_style_file(const char *filename)
static int compare_pipe_order(const char *scenario, const int32_t actual_imgid, const int32_t expected_imgid, char **failure_reason)
static int apply_style_to_image(const char *scenario, const char *style_name, const int32_t imgid, char **failure_reason)
static gboolean is_style_file(const char *filename)
static int run_style_scenario(const char *scenario_dir, const char *source_image_path, const char *style_file, char **failure_reason)
static int load_xmp_on_image(const char *scenario, const int32_t imgid, const char *xmp_path, char **failure_reason)
static void print_module_order_summary(const char *label, const int32_t imgid)
static void print_enabled_state_summary(const char *label, const int32_t imgid)
#define ANSEL_TEST_BINARY_DIR