diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-13 20:57:16 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-13 20:57:16 +0300 |
| commit | 0795d3cf926d0d937f25fcc699f5f674bf618c72 (patch) | |
| tree | 48e3fcf44a7c5213c11c63c1f69eb8b336edb325 /tests | |
| parent | 48aa1b3cdbf27be2e016e8b532687226f9249043 (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.build | 30 | ||||
| -rw-r--r-- | tests/test_grid_cull.c | 212 | ||||
| -rw-r--r-- | tests/test_texturecache.c | 5 | ||||
| -rw-r--r-- | tests/test_thumbnail.c | 178 | ||||
| -rw-r--r-- | tests/test_trash.c | 180 |
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 |
