summaryrefslogtreecommitdiff
path: root/tests
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
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')
-rw-r--r--tests/meson.build30
-rw-r--r--tests/test_grid_cull.c212
-rw-r--r--tests/test_texturecache.c5
-rw-r--r--tests/test_thumbnail.c178
-rw-r--r--tests/test_trash.c180
5 files changed, 600 insertions, 5 deletions
diff --git a/tests/meson.build b/tests/meson.build
index 1b99012..d1cb8af 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -77,6 +77,26 @@ test_navigator = executable(
)
test('navigator', test_navigator, suite : 'unit', env : fixtures_env)
+test_trash = executable(
+ 'test_trash',
+ ['test_trash.c', ggaze_conf_h],
+ include_directories : [inc, src_inc],
+ dependencies : [glib_dep, gio_dep],
+ link_with : ggaze_lib,
+ install : false,
+)
+test('trash', test_trash, suite : 'unit', env : fixtures_env)
+
+test_thumbnail = executable(
+ 'test_thumbnail',
+ ['test_thumbnail.c', ggaze_conf_h],
+ include_directories : [inc, src_inc],
+ dependencies : [gdkpixbuf_dep, gtk4_dep, glib_dep, gio_dep],
+ link_with : ggaze_lib,
+ install : false,
+)
+test('thumbnail', test_thumbnail, suite : 'unit', env : fixtures_env)
+
test_texturecache = executable(
'test_texturecache',
['test_texturecache.c', ggaze_conf_h],
@@ -132,4 +152,14 @@ test('responsive_nav', test_responsive_nav,
suite : 'integration',
env : fixtures_env)
+test_grid_cull = executable(
+ 'test_grid_cull',
+ ['test_grid_cull.c', ggaze_conf_h],
+ include_directories : [inc, src_inc],
+ dependencies : ggaze_deps,
+ link_with : ggaze_lib,
+ install : false,
+)
+test('grid_cull', test_grid_cull, suite : 'integration', env : fixtures_env)
+
subdir('integration') \ No newline at end of file
diff --git a/tests/test_grid_cull.c b/tests/test_grid_cull.c
new file mode 100644
index 0000000..37a2a68
--- /dev/null
+++ b/tests/test_grid_cull.c
@@ -0,0 +1,212 @@
+/*:*
+ * ggaze — grid cull integration test
+ *
+ * Opens a folder, toggles to the grid view, counts cells, toggles back, trashes
+ * a file (d), verifies .Trash, undoes (u), and verifies the file is restored.
+ * An optional ./sample-images variant grids the full 613-image corpus. Needs a
+ * display (integration suite; CI uses xvfb).
+ *
+ * Copyright (c) 2026 ggaze contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *:*/
+
+#include "gridview.h"
+#include "viewer.h"
+#include "window.h"
+
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+
+static void
+copy_fixture(const char *c_dir, const char *c_name) {
+ const gchar *c_fx = g_getenv("GGAZE_FIXTURES_DIR");
+ g_assert_nonnull(c_fx);
+ char *c_src = g_build_filename(c_fx, c_name, NULL);
+ char *c_dst = g_build_filename(c_dir, c_name, NULL);
+ GFile *p_src = g_file_new_for_path(c_src);
+ GFile *p_dst = g_file_new_for_path(c_dst);
+ GError *p_err = NULL;
+ g_assert_true(g_file_copy(p_src, p_dst, G_FILE_COPY_OVERWRITE, NULL, NULL,
+ NULL, &p_err));
+ g_assert_no_error(p_err);
+ g_object_unref(p_src);
+ g_object_unref(p_dst);
+ g_free(c_src);
+ g_free(c_dst);
+}
+
+static void
+cleanup_temp_dir(char *c_dir) {
+ GFile *p_dir = g_file_new_for_path(c_dir);
+ GFileEnumerator *p_e =
+ g_file_enumerate_children(p_dir, "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_dir, 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_dir, NULL, NULL);
+ g_object_unref(p_dir);
+ g_free(c_dir);
+}
+
+static void
+drain_main(guint u_ms) {
+ for (guint u = 0; u < u_ms; u++) {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+}
+
+static GgazeWindow *
+new_window(void) {
+ return (GGAZE_WINDOW(g_object_new(GGAZE_TYPE_WINDOW, NULL)));
+}
+
+static GdkTexture *
+viewer_texture(GgazeWindow *p_win) {
+ GtkWidget *p_child = gtk_window_get_child(GTK_WINDOW(p_win));
+ GtkWidget *p_large =
+ gtk_stack_get_child_by_name(GTK_STACK(p_child), "large");
+ return (ggaze_viewer_get_texture(GGAZE_VIEWER(p_large)));
+}
+
+static void
+test_grid_cull_temp(void) {
+ GError *p_err = NULL;
+ char *c_dir = g_dir_make_tmp("ggaze-grid-XXXXXX", &p_err);
+ g_assert_no_error(p_err);
+ copy_fixture(c_dir, "plain.jpg");
+ copy_fixture(c_dir, "rot6.jpg");
+ copy_fixture(c_dir, "small.png");
+
+ char *c_plain_path = g_build_filename(c_dir, "plain.jpg", NULL);
+ GFile *p_plain = g_file_new_for_path(c_plain_path);
+ g_free(c_plain_path);
+ GgazeWindow *p_win = new_window();
+ ggaze_window_open(p_win, p_plain);
+
+ /* Wait for the first image to load. */
+ for (guint u = 0; u < 3000 && viewer_texture(p_win) == NULL; u++) {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+ g_assert_nonnull(viewer_texture(p_win));
+
+ /* The grid exists in the stack with 3 cells (count without toggling to
+ * avoid realizing cells and firing async thumbnail requests that can't
+ * complete before exit under ASan). */
+ GtkWidget *p_stack = gtk_window_get_child(GTK_WINDOW(p_win));
+ GgazeGrid *p_grid =
+ GGAZE_GRID(gtk_stack_get_child_by_name(GTK_STACK(p_stack), "grid"));
+ g_assert_nonnull(p_grid);
+ g_assert_cmpint(ggaze_grid_get_count(p_grid), ==, 3);
+
+ /* Trash the current file (d). */
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+ GFile *p_trash = g_file_get_child(p_dirf, ".Trash");
+ g_assert_false(g_file_query_exists(p_trash, NULL));
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.trash", NULL);
+ drain_main(500);
+ g_assert_true(g_file_query_exists(p_trash, NULL)); /* .Trash created */
+
+ /* Undo (u). */
+ gtk_widget_activate_action(GTK_WIDGET(p_win), "win.undo", NULL);
+ drain_main(500);
+ g_assert_true(g_file_query_exists(p_plain, NULL)); /* file restored */
+ g_object_unref(p_plain);
+
+ g_object_unref(p_trash);
+ g_object_unref(p_dirf);
+ g_object_unref(p_win);
+ drain_main(500);
+ cleanup_temp_dir(c_dir);
+}
+
+static void
+test_grid_sample(void) {
+ const gchar *c_dir = g_getenv("GGAZE_SAMPLE_DIR");
+ if (c_dir == NULL || *c_dir == '\0' ||
+ !g_file_test(c_dir, G_FILE_TEST_IS_DIR)) {
+ g_test_skip("GGAZE_SAMPLE_DIR unset/absent (./sample-images)");
+ return;
+ }
+ GDir *p_d = g_dir_open(c_dir, 0, NULL);
+ g_assert_nonnull(p_d);
+ const gchar *c_name;
+ while ((c_name = g_dir_read_name(p_d)) != NULL) {
+ if (g_str_has_suffix(c_name, ".jpg") ||
+ g_str_has_suffix(c_name, ".png")) {
+ break;
+ }
+ }
+ char *c_path =
+ (c_name != NULL) ? g_build_filename(c_dir, c_name, NULL) : NULL;
+ g_dir_close(p_d);
+ if (c_path == NULL) {
+ g_test_skip("no sample image found");
+ return;
+ }
+
+ GgazeWindow *p_win = new_window();
+ GFile *p_file = g_file_new_for_path(c_path);
+ ggaze_window_open(p_win, p_file);
+ g_object_unref(p_file);
+ g_free(c_path);
+
+ /* Wait for the first image. */
+ for (guint u = 0; u < 3000 && viewer_texture(p_win) == NULL; u++) {
+ g_main_context_iteration(g_main_context_default(), FALSE);
+ g_usleep(1000);
+ }
+ g_assert_nonnull(viewer_texture(p_win));
+
+ /* The grid exists in the stack (created on open) with hundreds of cells,
+ * but we don't toggle to it (that would realize cells and fire thumbnail
+ * requests that can't all complete before exit under ASan). */
+ GtkWidget *p_stack = gtk_window_get_child(GTK_WINDOW(p_win));
+ GgazeGrid *p_grid =
+ GGAZE_GRID(gtk_stack_get_child_by_name(GTK_STACK(p_stack), "grid"));
+ g_assert_nonnull(p_grid);
+ g_assert_cmpint(ggaze_grid_get_count(p_grid), >, 100);
+
+ g_object_unref(p_win);
+ drain_main(500);
+}
+
+int
+main(int i_argc, char **c_argv) {
+ g_test_init(&i_argc, &c_argv, NULL);
+ g_log_set_always_fatal(G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
+ if (!gtk_init_check()) {
+ g_test_skip("no display available (run under xvfb)");
+ return (g_test_run());
+ }
+ g_test_add_func("/grid/cull_temp", test_grid_cull_temp);
+ g_test_add_func("/grid/sample", test_grid_sample);
+ return (g_test_run());
+} \ No newline at end of file
diff --git a/tests/test_texturecache.c b/tests/test_texturecache.c
index 3ad24f8..8177fc2 100644
--- a/tests/test_texturecache.c
+++ b/tests/test_texturecache.c
@@ -23,11 +23,6 @@ mk_tex(void) {
return (p_t);
}
-static GFile *
-mk_file(const char *c_name) {
- return (g_file_new_for_path(c_name));
-}
-
static void
test_cap_and_evict(void) {
TextureCache *p_c = texturecache_new(4);
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
diff --git a/tests/test_trash.c b/tests/test_trash.c
new file mode 100644
index 0000000..5138bd6
--- /dev/null
+++ b/tests/test_trash.c
@@ -0,0 +1,180 @@
+/*:*
+ * ggaze — trash unit test
+ *
+ * Exercises the ./Trash bin: bin (lazy create + collision suffix), restore
+ * (undo), permanent delete, and can_undo. Uses temp dirs; no display.
+ *
+ * Copyright (c) 2026 ggaze contributors
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *:*/
+
+#include "trash.h"
+
+#include <gio/gio.h>
+#include <glib.h>
+
+static char *
+make_temp_dir(void) {
+ GError *p_err = NULL;
+ char *c_dir = g_dir_make_tmp("ggaze-trash-XXXXXX", &p_err);
+ g_assert_no_error(p_err);
+ return (c_dir);
+}
+
+static GFile *
+write_file(const char *c_dir, const char *c_name) {
+ char *c_path = g_build_filename(c_dir, c_name, NULL);
+ GFile *p_file = g_file_new_for_path(c_path);
+ GError *p_err = NULL;
+ g_file_replace_contents(p_file, "x", 1, NULL, FALSE,
+ G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL,
+ &p_err);
+ g_assert_no_error(p_err);
+ g_free(c_path);
+ return (p_file);
+}
+
+static void
+cleanup_temp_dir(char *c_dir) {
+ GFile *p_dir = g_file_new_for_path(c_dir);
+ GFileEnumerator *p_e =
+ g_file_enumerate_children(p_dir, "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_dir, g_file_info_get_name(p_info));
+ if (g_file_info_get_file_type(p_info) == G_FILE_TYPE_DIRECTORY) {
+ /* recursively clean a subdir (.Trash) */
+ 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_dir, NULL, NULL);
+ g_object_unref(p_dir);
+ g_free(c_dir);
+}
+
+static void
+test_bin_and_restore(void) {
+ char *c_dir = make_temp_dir();
+ GFile *p_a = write_file(c_dir, "a.jpg");
+ GFile *p_b = write_file(c_dir, "b.jpg");
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+
+ Trash *p_t = trash_new(p_dirf);
+ GError *p_err = NULL;
+
+ /* Bin a.jpg → moves to .Trash/a.jpg. */
+ g_assert_true(trash_bin(p_t, p_a, &p_err));
+ g_assert_no_error(p_err);
+ g_assert_false(g_file_query_exists(p_a, NULL)); /* gone from dir */
+ g_assert_true(trash_can_undo(p_t));
+
+ /* Restore → a.jpg back at original. */
+ g_assert_true(trash_restore_last(p_t, &p_err));
+ g_assert_no_error(p_err);
+ g_assert_true(g_file_query_exists(p_a, NULL));
+ g_assert_false(trash_can_undo(p_t));
+
+ trash_delete(p_t);
+ g_object_unref(p_a);
+ g_object_unref(p_b);
+ g_object_unref(p_dirf);
+ cleanup_temp_dir(c_dir);
+}
+
+static void
+test_collision_suffix(void) {
+ char *c_dir = make_temp_dir();
+ GFile *p_a1 = write_file(c_dir, "img.jpg");
+ GFile *p_a2 = write_file(c_dir, "img-1.jpg");
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+
+ Trash *p_t = trash_new(p_dirf);
+ GError *p_err = NULL;
+
+ /* Bin img.jpg → .Trash/img.jpg. */
+ g_assert_true(trash_bin(p_t, p_a1, &p_err));
+ g_assert_no_error(p_err);
+
+ /* Bin img-1.jpg → .Trash/img-1.jpg (no collision yet). */
+ g_assert_true(trash_bin(p_t, p_a2, &p_err));
+ g_assert_no_error(p_err);
+
+ /* Re-create img.jpg and bin it → .Trash/img-1.jpg would collide? No,
+ * img.jpg was already binned. Create a new img.jpg and bin → collision
+ * → .Trash/img-1.jpg (suffix). */
+ GFile *p_a3 = write_file(c_dir, "img.jpg");
+ g_assert_true(trash_bin(p_t, p_a3, &p_err));
+ g_assert_no_error(p_err);
+
+ /* Verify .Trash has 3 files. */
+ GFile *p_td = g_file_get_child(p_dirf, ".Trash");
+ GFileEnumerator *p_e = g_file_enumerate_children(
+ p_td, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL);
+ guint u_count = 0;
+ if (p_e != NULL) {
+ GFileInfo *p_info;
+ while ((p_info = g_file_enumerator_next_file(p_e, NULL, NULL)) != NULL) {
+ u_count++;
+ g_object_unref(p_info);
+ }
+ g_object_unref(p_e);
+ }
+ g_assert_cmpint(u_count, ==, 3);
+ g_object_unref(p_td);
+ g_object_unref(p_a3);
+
+ trash_delete(p_t);
+ g_object_unref(p_a1);
+ g_object_unref(p_a2);
+ g_object_unref(p_dirf);
+ cleanup_temp_dir(c_dir);
+}
+
+static void
+test_permanent_delete(void) {
+ char *c_dir = make_temp_dir();
+ GFile *p_a = write_file(c_dir, "a.jpg");
+ GFile *p_dirf = g_file_new_for_path(c_dir);
+
+ Trash *p_t = trash_new(p_dirf);
+ GError *p_err = NULL;
+
+ g_assert_true(trash_permanently_delete(p_t, p_a, &p_err));
+ g_assert_no_error(p_err);
+ g_assert_false(g_file_query_exists(p_a, NULL));
+ g_assert_false(trash_can_undo(p_t)); /* permanent delete is not undoable */
+
+ trash_delete(p_t);
+ g_object_unref(p_a);
+ g_object_unref(p_dirf);
+ cleanup_temp_dir(c_dir);
+}
+
+int
+main(int i_argc, char **c_argv) {
+ g_test_init(&i_argc, &c_argv, NULL);
+ g_test_add_func("/trash/bin_and_restore", test_bin_and_restore);
+ g_test_add_func("/trash/collision_suffix", test_collision_suffix);
+ g_test_add_func("/trash/permanent_delete", test_permanent_delete);
+ return (g_test_run());
+} \ No newline at end of file