summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-21 18:39:47 +0300
committerPaul Buetow <paul@buetow.org>2026-07-21 18:39:47 +0300
commitcfda8297cc4432a77e3f7e8679753a908e9f5759 (patch)
treebc8c1469989bb55656c93a65c277ab86e17adf9a /tests
parentdc127b5089656944dab7ab72799d0755ca122803 (diff)
enhance: wire up GEGL image filters (a cycle / s save) + fix export bugs
The enhancer module existed (GEGL presets: Auto-fix, Brightness, Contrast, Saturation, Warm, Cool, Sharpen, Denoise) but was never connected to the app and GEGL was never initialized. Wire it up: - app.c: gegl_init() once in GApplication::startup (GEGL-gated). - enhancer.c: add enhancer_load(GFile) -> GeglBuffer (gegl:load) and enhancer_buffer_to_texture(GeglBuffer) -> GdkTexture (RGBA8 -> GdkMemoryTexture) for the live preview bridge. - enhancer_export: pick the saver from the output extension (jpg/png/webp) instead of always gegl:jpg-save (ju0 - was writing JPEG bytes into .png); verify the save actually produced a non-empty newer file instead of trusting g_file_test(EXISTS) on a pre-existing path (ku0 - false success). - window.c: win.enhance (a) cycles the active preset and previews it on the current image (switches to large view, loads via GEGL, applies, shows the texture); win.enhance-save (s) exports <stem>-enhanced.<ext> with the active preset, never overwriting the original. Preview resets on navigation; the title shows the active preset. - shortcuts.c: a -> win.enhance, s -> win.enhance-save; ? overlay Enhance group. - tests: test_enhancer gains load_and_to_texture, export_format (PNG/JPEG signature checks), export_real_success (ku0 - parent-missing and pre-existing-directory both return FALSE). LSAN suppressions cover GEGL's jpg-load plugin leak. test_shortcut full-table updated (25 rows). Synchronous apply (may briefly block on large images) and a navigate-away dirty prompt are deliberately out of scope for v1.
Diffstat (limited to 'tests')
-rw-r--r--tests/lsan_suppressions.txt5
-rw-r--r--tests/meson.build2
-rw-r--r--tests/test_enhancer.c185
-rw-r--r--tests/test_shortcut.c15
4 files changed, 199 insertions, 8 deletions
diff --git a/tests/lsan_suppressions.txt b/tests/lsan_suppressions.txt
index ac2c4db..27d2450 100644
--- a/tests/lsan_suppressions.txt
+++ b/tests/lsan_suppressions.txt
@@ -7,3 +7,8 @@ leak:babl
leak:gegl_node_new_child
leak:gegl_node_new
leak:gegl_operation
+# GEGL's jpg-load plugin leaks a gio source buffer during jpeg_read_header
+# (third-party, in jpg-load.so).
+leak:gio_source_init
+leak:gegl_jpg_load
+leak:jpg-load
diff --git a/tests/meson.build b/tests/meson.build
index 6a93277..e895210 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -147,7 +147,7 @@ if gegl_dep.found()
test_enhancer = executable(
'test_enhancer', ['test_enhancer.c', ggaze_conf_h],
include_directories : [inc, src_inc],
- dependencies : [glib_dep, gegl_dep],
+ dependencies : [glib_dep, gegl_dep, gtk4_dep],
link_with : ggaze_lib, install : false,
)
enhancer_env = environment()
diff --git a/tests/test_enhancer.c b/tests/test_enhancer.c
index 83768a2..1445c41 100644
--- a/tests/test_enhancer.c
+++ b/tests/test_enhancer.c
@@ -79,11 +79,196 @@ test_export(void) {
g_free(tmp);
}
+/* /enhancer/load_and_to_texture: load a fixture via the gegl:load bridge and
+ * convert it to a GdkTexture (no display needed). */
+static void
+test_load_and_to_texture(void) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_path = g_build_filename(c_fx, "plain.jpg", NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ g_free(c_path);
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ g_assert_no_error(p_err);
+ g_assert_nonnull(p_buf);
+ g_assert_cmpint(gegl_buffer_get_width(p_buf), >, 0);
+ g_assert_cmpint(gegl_buffer_get_height(p_buf), >, 0);
+
+ GdkTexture *p_tex = enhancer_buffer_to_texture(p_buf, &p_err);
+ g_assert_no_error(p_err);
+ g_assert_nonnull(p_tex);
+ g_assert_true(GDK_IS_TEXTURE(p_tex));
+ g_assert_cmpint(gdk_texture_get_width(p_tex), >, 0);
+ g_assert_cmpint(gdk_texture_get_height(p_tex), >, 0);
+
+ g_object_unref(p_tex);
+ g_object_unref(p_buf);
+ g_object_unref(p_file);
+}
+
+/* /enhancer/export_format: apply Auto-fix and export to .png and .jpg,
+ * asserting the file signatures (catches ju0 — never write JPEG into a .png).
+ * Skips gracefully if the saver op is unavailable. */
+static void
+test_export_format(void) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_path = g_build_filename(c_fx, "plain.jpg", NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ g_free(c_path);
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ g_assert_nonnull(p_buf);
+
+ Enhancer *e = enhancer_new();
+ const EnhancerPreset *preset =
+ g_ptr_array_index((GPtrArray *)enhancer_get_presets(e), 0); /* Auto-fix */
+ char *tmp = g_dir_make_tmp("ggaze-fmt-XXXXXX", NULL);
+
+ /* PNG */
+ {
+ char *c_p = g_build_filename(tmp, "out.png", NULL);
+ GFile *p_out = g_file_new_for_path(c_p);
+ g_clear_error(&p_err);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ if (ok) {
+ gchar *data = NULL;
+ gsize len = 0;
+ g_assert_true(g_file_get_contents(c_p, &data, &len, NULL));
+ g_assert_cmpint(len, >=, 8);
+ g_assert_cmpmem(data, 8, "\x89PNG\r\n\x1a\n", 8);
+ g_free(data);
+ } else {
+ g_clear_error(&p_err);
+ }
+ g_object_unref(p_out);
+ g_free(c_p);
+ }
+
+ /* JPEG */
+ {
+ char *c_p = g_build_filename(tmp, "out.jpg", NULL);
+ GFile *p_out = g_file_new_for_path(c_p);
+ g_clear_error(&p_err);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ if (ok) {
+ gchar *data = NULL;
+ gsize len = 0;
+ g_assert_true(g_file_get_contents(c_p, &data, &len, NULL));
+ g_assert_cmpint(len, >=, 2);
+ g_assert_cmpmem(data, 2, "\xff\xd8", 2);
+ g_free(data);
+ } else {
+ g_clear_error(&p_err);
+ }
+ g_object_unref(p_out);
+ g_free(c_p);
+ }
+
+ g_object_unref(p_buf);
+ g_object_unref(p_file);
+ enhancer_delete(e);
+
+ /* Cleanup tmp. */
+ GFile *td = g_file_new_for_path(tmp);
+ GFileEnumerator *en = g_file_enumerate_children(
+ td, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (en) {
+ GFileInfo *i;
+ while ((i = g_file_enumerator_next_file(en, NULL, NULL))) {
+ GFile *c = g_file_get_child(td, g_file_info_get_name(i));
+ g_file_delete(c, NULL, NULL);
+ g_object_unref(c);
+ g_object_unref(i);
+ }
+ g_object_unref(en);
+ }
+ g_file_delete(td, NULL, NULL);
+ g_object_unref(td);
+ g_free(tmp);
+}
+
+/* /enhancer/export_real_success (ku0): a save that produces no real file must
+ * return FALSE with a GError, not TRUE-on-pre-existence. Two cases: (a) an
+ * output path whose parent directory does not exist, and (b) an output path
+ * that is a pre-existing directory (named like a supported extension so a
+ * saver op is actually selected). */
+static void
+test_export_real_success(void) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_path = g_build_filename(c_fx, "plain.jpg", NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ g_free(c_path);
+
+ GError *p_err = NULL;
+ GeglBuffer *p_buf = enhancer_load(p_file, &p_err);
+ g_assert_nonnull(p_buf);
+
+ Enhancer *e = enhancer_new();
+ const EnhancerPreset *preset =
+ g_ptr_array_index((GPtrArray *)enhancer_get_presets(e), 0);
+ char *tmp = g_dir_make_tmp("ggaze-real-XXXXXX", NULL);
+
+ /* (a) parent dir does not exist: the saver cannot write, no file appears.
+ * GEGL emits a g_warning on the failed save; relax the fatal mask so
+ * enhancer_export can return FALSE and be asserted instead of aborting. */
+ {
+ char *c_bad = g_build_filename(tmp, "no-such-dir", "out.png", NULL);
+ GFile *p_out = g_file_new_for_path(c_bad);
+ g_clear_error(&p_err);
+ GLogLevelFlags old_mask = g_log_set_always_fatal(G_LOG_LEVEL_ERROR);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ g_log_set_always_fatal(old_mask);
+ g_assert_false(ok);
+ g_assert_nonnull(p_err);
+ g_assert_cmpint(p_err->code, ==, G_IO_ERROR_FAILED);
+ g_clear_error(&p_err);
+ g_object_unref(p_out);
+ g_free(c_bad);
+ }
+
+ /* (b) output path is a pre-existing directory named out.png: the saver op
+ * is selected (extension matches), the write fails (EISDIR), and the
+ * pre-existing directory must NOT count as a successful save. */
+ {
+ char *c_dir = g_build_filename(tmp, "out.png", NULL);
+ g_assert_true(g_mkdir_with_parents(c_dir, 0700) == 0);
+ GFile *p_out = g_file_new_for_path(c_dir);
+ g_clear_error(&p_err);
+ GLogLevelFlags old_mask = g_log_set_always_fatal(G_LOG_LEVEL_ERROR);
+ gboolean ok = enhancer_export(e, p_buf, preset, p_out, &p_err);
+ g_log_set_always_fatal(old_mask);
+ g_assert_false(ok);
+ g_assert_nonnull(p_err);
+ g_clear_error(&p_err);
+ g_object_unref(p_out);
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+ g_assert_true(g_file_delete(p_dirf, NULL, NULL));
+ g_object_unref(p_dirf);
+ g_free(c_dir);
+ }
+
+ g_object_unref(p_buf);
+ g_object_unref(p_file);
+ enhancer_delete(e);
+ GFile *p_tmpf = g_file_new_for_path(tmp);
+ g_file_delete(p_tmpf, NULL, NULL);
+ g_object_unref(p_tmpf);
+ g_free(tmp);
+}
+
int
main(int argc, char **argv) {
gegl_init(&argc, &argv);
g_test_init(&argc, &argv, NULL);
g_test_add_func("/enhancer/builtin_presets", test_builtin_presets);
g_test_add_func("/enhancer/export", test_export);
+ g_test_add_func("/enhancer/load_and_to_texture", test_load_and_to_texture);
+ g_test_add_func("/enhancer/export_format", test_export_format);
+ g_test_add_func("/enhancer/export_real_success", test_export_real_success);
return g_test_run();
} \ No newline at end of file
diff --git a/tests/test_shortcut.c b/tests/test_shortcut.c
index e57d361..c9a66a9 100644
--- a/tests/test_shortcut.c
+++ b/tests/test_shortcut.c
@@ -328,11 +328,12 @@ test_shortcut_keypath_toggle_and_back(void) {
static void
test_shortcut_full_table_registered(void) {
static const char *ACTIONS[] = {
- "win.prev", "win.next", "win.first", "win.last",
- "win.open", "win.quit", "win.trash", "win.delete",
- "win.undo", "win.toggle-view", "win.mark", "win.mark-all",
- "win.shortcuts", "win.zoom-in", "win.zoom-out", "win.fullscreen",
- "win.slideshow", "win.info", "win.back",
+ "win.prev", "win.next", "win.first", "win.last",
+ "win.open", "win.quit", "win.trash", "win.delete",
+ "win.undo", "win.toggle-view", "win.mark", "win.mark-all",
+ "win.shortcuts", "win.zoom-in", "win.zoom-out", "win.fullscreen",
+ "win.slideshow", "win.info", "win.back", "win.enhance",
+ "win.enhance-save",
};
GgazeWindow *p_win = new_window();
GtkShortcutController *p_sc = find_shortcut_controller(GTK_WIDGET(p_win));
@@ -342,9 +343,9 @@ test_shortcut_full_table_registered(void) {
g_assert_nonnull(p_s);
g_object_unref(p_s);
}
- /* The SHORTCUTS[] table has 23 rows now (some actions appear twice, e.g.
+ /* The SHORTCUTS[] table has 25 rows now (some actions appear twice, e.g.
* win.prev for h and Left; win.zoom-in for plus and equal). */
- g_assert_cmpint(g_list_model_get_n_items(G_LIST_MODEL(p_sc)), ==, 23);
+ g_assert_cmpint(g_list_model_get_n_items(G_LIST_MODEL(p_sc)), ==, 25);
g_object_unref(p_sc);
g_object_unref(p_win);
drain_main(200);