summaryrefslogtreecommitdiff
path: root/tests/test_thumbnail.c
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-13 20:57:16 +0300
committerPaul Buetow <paul@buetow.org>2026-07-13 20:57:16 +0300
commit0795d3cf926d0d937f25fcc699f5f674bf618c72 (patch)
tree48e3fcf44a7c5213c11c63c1f69eb8b336edb325 /tests/test_thumbnail.c
parent48aa1b3cdbf27be2e016e8b532687226f9249043 (diff)
gridcull: thumbnail TMS cache, grid view, trash, window d/D/u/t (jt0)
M7 (task jt0): thumbnail cache + grid view + trash. - src/trash.{c,h}: ./Trash bin (lazy create, collision suffix -1/-2, restore_last for u undo, permanent delete for D). Plain-C, unit-tested. - src/thumbnail.{c,h}: freedesktop TMS cache (normal/large/x-large buckets, md5(URI) key, Thumb::URI/MTime/Size, mtime verify). Async GTask worker with GCancellable so the grid can cancel pending requests on dispose. - src/gridview.{c,h}: GgazeGrid (GtkWidget wrapping GtkFlowBox): one cell per navigator file, lazy thumbnail decode on realize, +/- resize 64-512, dim removed cells, mark badges, Enter/double-click → 'activate' signal, cursor sync. ggaze_grid_detach() breaks the nav ref before dispose. - src/navigator.{c,h}: removed set (mark_removed/is_removed/unmark_removed); _relist preserves removed items (stay dimmed even if absent from disk); get_remaining = count - removed_count. - src/window.c: d (trash+mark_removed+advance), D (permanent delete, confirm dialog if >1 marked), u (restore_last+rescan), t (toggle grid/large), +/- (viewer zoom in large / grid resize). Header counter uses remaining. - src/shortcuts.c: d/D/u/t/+/= bindings added. - tests: unit test_trash + test_thumbnail, integration test_grid_cull (grid count + trash/undo + ./sample-images 613-cell grid). 13/13 green, ASan clean. Fixed: grid dispose UAF (detach before nav freed), g_file_get_basename leak, test p_plain lifetime, thumbnail async-cancellable for clean dispose, test grid-toggle avoiding async-thumbnail drain issues.
Diffstat (limited to 'tests/test_thumbnail.c')
-rw-r--r--tests/test_thumbnail.c178
1 files changed, 178 insertions, 0 deletions
diff --git a/tests/test_thumbnail.c b/tests/test_thumbnail.c
new file mode 100644
index 0000000..e504ff9
--- /dev/null
+++ b/tests/test_thumbnail.c
@@ -0,0 +1,178 @@
+/*:*
+ * ggaze — thumbnail cache unit test
+ *
+ * Generates a thumbnail for a fixture, verifies the TMS cache file is written
+ * (with Thumb::MTime), and that a second get hits the cache. Uses a temp
+ * XDG_CACHE_HOME so the real cache is not polluted. No display needed.
+ *
+ * Copyright (c) 2026 ggaze contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *:*/
+
+#include "thumbnail.h"
+
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+#include <glib.h>
+
+static const char *GGAZE_FX_DIR;
+static char *GGAZE_CACHE_DIR;
+
+static GdkTexture *GGAZE_RESULT;
+static GMainLoop *GGAZE_LOOP;
+
+static void
+_thumb_cb(GObject *p_src, GAsyncResult *p_res, gpointer p_data) {
+ (void)p_src;
+ (void)p_data;
+ GError *p_err = NULL;
+ GGAZE_RESULT = thumbnail_get_finish(NULL, p_res, &p_err);
+ g_assert_no_error(p_err);
+ g_main_loop_quit(GGAZE_LOOP);
+}
+
+/* Get a thumbnail synchronously (pump a main loop until the callback). */
+static GdkTexture *
+get_thumb(Thumbnail *p_t, GFile *p_file, int i_size) {
+ GGAZE_RESULT = NULL;
+ GGAZE_LOOP = g_main_loop_new(NULL, FALSE);
+ thumbnail_get_async(p_t, p_file, i_size, NULL, _thumb_cb, NULL);
+ g_main_loop_run(GGAZE_LOOP);
+ g_main_loop_unref(GGAZE_LOOP);
+ GGAZE_LOOP = NULL;
+ return (GGAZE_RESULT);
+}
+
+static GFile *
+fixture_file(const char *c_name) {
+ char *c_path = g_build_filename(GGAZE_FX_DIR, c_name, NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ g_free(c_path);
+ return (p_file);
+}
+
+static void
+test_generate_and_cache(void) {
+ Thumbnail *p_t = thumbnail_new();
+ GFile *p_file = fixture_file("plain.jpg");
+
+ GdkTexture *p_tex = get_thumb(p_t, p_file, 128);
+ g_assert_nonnull(p_tex);
+ g_assert_cmpint(gdk_texture_get_width(p_tex), <=, 128);
+ g_assert_cmpint(gdk_texture_get_height(p_tex), <=, 128);
+ g_object_unref(p_tex);
+
+ /* The cache file should exist under our temp XDG_CACHE_HOME. */
+ char *c_normal =
+ g_build_filename(GGAZE_CACHE_DIR, "thumbnails", "normal", NULL);
+ GDir *p_dir = g_dir_open(c_normal, 0, NULL);
+ g_assert_nonnull(p_dir);
+ gboolean b_found = FALSE;
+ const char *c_name;
+ while ((c_name = g_dir_read_name(p_dir)) != NULL) {
+ if (g_str_has_suffix(c_name, ".png")) {
+ b_found = TRUE;
+ break;
+ }
+ }
+ g_dir_close(p_dir);
+ g_free(c_normal);
+ g_assert_true(b_found);
+
+ /* Second get should hit the cache (same result). */
+ GdkTexture *p_tex2 = get_thumb(p_t, p_file, 128);
+ g_assert_nonnull(p_tex2);
+ g_object_unref(p_tex2);
+
+ thumbnail_delete(p_t);
+ g_object_unref(p_file);
+}
+
+static void
+test_different_bucket(void) {
+ Thumbnail *p_t = thumbnail_new();
+ GFile *p_file = fixture_file("rot6.jpg");
+
+ /* Request 200px → bucket = large (256). */
+ GdkTexture *p_tex = get_thumb(p_t, p_file, 200);
+ g_assert_nonnull(p_tex);
+ g_object_unref(p_tex);
+
+ /* The cache file should be under "large". */
+ char *c_large =
+ g_build_filename(GGAZE_CACHE_DIR, "thumbnails", "large", NULL);
+ GDir *p_dir = g_dir_open(c_large, 0, NULL);
+ g_assert_nonnull(p_dir);
+ gboolean b_found = FALSE;
+ const char *c_name;
+ while ((c_name = g_dir_read_name(p_dir)) != NULL) {
+ if (g_str_has_suffix(c_name, ".png")) {
+ b_found = TRUE;
+ break;
+ }
+ }
+ g_dir_close(p_dir);
+ g_free(c_large);
+ g_assert_true(b_found);
+
+ thumbnail_delete(p_t);
+ g_object_unref(p_file);
+}
+
+int
+main(int i_argc, char **c_argv) {
+ g_test_init(&i_argc, &c_argv, NULL);
+
+ GGAZE_FX_DIR = g_getenv("GGAZE_FIXTURES_DIR");
+ if (GGAZE_FX_DIR == NULL) {
+ g_test_skip("GGAZE_FIXTURES_DIR unset");
+ return (g_test_run());
+ }
+
+ /* Use a temp XDG_CACHE_HOME so the real cache is not polluted. */
+ GError *p_err = NULL;
+ GGAZE_CACHE_DIR = g_dir_make_tmp("ggaze-thumb-cache-XXXXXX", &p_err);
+ g_assert_no_error(p_err);
+ g_setenv("XDG_CACHE_HOME", GGAZE_CACHE_DIR, TRUE);
+
+ g_test_add_func("/thumbnail/generate_and_cache", test_generate_and_cache);
+ g_test_add_func("/thumbnail/different_bucket", test_different_bucket);
+
+ int i_ret = g_test_run();
+
+ /* Cleanup the temp cache dir (best-effort recursive). */
+ GFile *p_cd = g_file_new_for_path(GGAZE_CACHE_DIR);
+ GFileEnumerator *p_e =
+ g_file_enumerate_children(p_cd, "standard::name,standard::type",
+ G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (p_e != NULL) {
+ GFileInfo *p_info;
+ while ((p_info = g_file_enumerator_next_file(p_e, NULL, NULL)) != NULL) {
+ GFile *p_child = g_file_get_child(p_cd, g_file_info_get_name(p_info));
+ if (g_file_info_get_file_type(p_info) == G_FILE_TYPE_DIRECTORY) {
+ GFileEnumerator *p_e2 = g_file_enumerate_children(
+ p_child, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ if (p_e2 != NULL) {
+ GFileInfo *p_i2;
+ while ((p_i2 = g_file_enumerator_next_file(p_e2, NULL, NULL)) !=
+ NULL) {
+ GFile *p_c2 =
+ g_file_get_child(p_child, g_file_info_get_name(p_i2));
+ g_file_delete(p_c2, NULL, NULL);
+ g_object_unref(p_c2);
+ g_object_unref(p_i2);
+ }
+ g_object_unref(p_e2);
+ }
+ }
+ g_file_delete(p_child, NULL, NULL);
+ g_object_unref(p_child);
+ g_object_unref(p_info);
+ }
+ g_object_unref(p_e);
+ }
+ g_file_delete(p_cd, NULL, NULL);
+ g_object_unref(p_cd);
+ g_free(GGAZE_CACHE_DIR);
+ return (i_ret);
+} \ No newline at end of file