diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-12 21:13:45 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-12 21:13:45 +0300 |
| commit | 89727590e718d3628a0847689182e896df646b8a (patch) | |
| tree | 3106397d1b041d95ecfc04762548261f9d660d62 /tests | |
| parent | 198b0e8112905bc003f954fc9159cc3da2dfce09 (diff) | |
walkdir: navigator, shortcuts, window wiring, drop target, tests (ht0)
M2 (task ht0): walk the directory.
- src/navigator.{c,h}: GObject (no GtkWidget) holding the folder listing โ
image-MIME filter + extension fallback, hide-RAW-sidecars (decision #33),
sort name/time/size, cursor + prev/next/wrap, path-based marks (survive
re-sort, pruned on rescan/monitor, cleared on remove), nearest-fallback,
GFileMonitor debounced 250ms (decision #28), 'changed' signal. 92% gcov.
- src/shortcuts.{c,h}: GtkShortcutController (MANAGED) binding h/l/Left/Right/
g/G/o/q to win.* actions; one table all milestones add to.
- src/window.{c,h}: open file->parent+current / folder->first; h/l/g/G actions
-> nav -> load -> viewer; single GCancellable last-write-wins (g_file_equal
guard for M3 async); header title 'filename ยท n/total'; GtkDropTarget
(GDK_TYPE_FILE_LIST, many->first, decision Z); GtkFileDialog open.
- data/gschema: add hide-raw-sidecars key.
- tests: unit test_navigator (12 subtests), integration test_walk_folder
(temp fixtures walk + ./sample-images skip-if-absent). 8/8 green; ASan clean
(GIO_USE_VFS=local in fixtures_env).
Fixed during this milestone: a UAF (borrowed c_name used after unref), a
cancellable double-ref leak, time::modified UINT64 vs INT64, and several
test-harness leaks. Sub-agent review issues addressed (mark pruning on
rescan/monitor, remove-before-cursor test, recursive temp-dir cleanup,
callback _cb naming, connect-before-mutate).
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/meson.build | 24 | ||||
| -rw-r--r-- | tests/test_navigator.c | 483 | ||||
| -rw-r--r-- | tests/test_walk_folder.c | 196 | ||||
| -rw-r--r-- | tests/test_window.c | 18 |
4 files changed, 714 insertions, 7 deletions
diff --git a/tests/meson.build b/tests/meson.build index 76d4962..bd45863 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -14,8 +14,10 @@ # GGAZE_FIXTURES_DIR committed tests/fixtures/ (CI-portable baseline) # GGAZE_SAMPLE_DIR local ./sample-images (not git-tracked; tests skip # cleanly when absent so CI stays green) +# GIO_USE_VFS=local avoid gvfs daemon-VFS internal leaks under ASan fixtures_env = environment() +fixtures_env.set('GIO_USE_VFS', 'local') fixtures_env.set('GGAZE_FIXTURES_DIR', join_paths(meson.project_source_root(), 'tests', 'fixtures')) fixtures_env.set('GGAZE_SAMPLE_DIR', @@ -65,6 +67,16 @@ test_loader_pixbuf = executable( ) test('loader_pixbuf', test_loader_pixbuf, suite : 'unit', env : fixtures_env) +test_navigator = executable( + 'test_navigator', + ['test_navigator.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : [glib_dep, gio_dep], + link_with : ggaze_lib, + install : false, +) +test('navigator', test_navigator, suite : 'unit', env : fixtures_env) + # --- integration track (needs a display; CI uses xvfb-run) --- test_window = executable( 'test_window', @@ -74,7 +86,7 @@ test_window = executable( link_with : ggaze_lib, install : false, ) -test('window', test_window, suite : 'integration') +test('window', test_window, suite : 'integration', env : fixtures_env) test_open_and_show = executable( 'test_open_and_show', @@ -88,4 +100,14 @@ test('open_and_show', test_open_and_show, suite : 'integration', env : fixtures_env) +test_walk_folder = executable( + 'test_walk_folder', + ['test_walk_folder.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : ggaze_deps, + link_with : ggaze_lib, + install : false, +) +test('walk_folder', test_walk_folder, suite : 'integration', env : fixtures_env) + subdir('integration')
\ No newline at end of file diff --git a/tests/test_navigator.c b/tests/test_navigator.c new file mode 100644 index 0000000..e8528b1 --- /dev/null +++ b/tests/test_navigator.c @@ -0,0 +1,483 @@ +/*:* + * ggaze โ navigator unit test + * + * Exercises the Navigator over temp directories (no display, no GTK widgets): + * listing + image filter, RAW-sidecar hiding, sort (name/time/size), cursor + + * prev/next/wrap, marks (toggle/range/all/clear/survive-resort), rescan + + * nearest-fallback, navigator_remove, the GFileMonitor debounce path, the + * "changed" signal, and empty-dir accessors. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "navigator.h" + +#include <gio/gio.h> +#include <glib.h> +#include <stdlib.h> + +/* --- helpers ------------------------------------------------------------- */ +/* write_file/touch_file create a file on disk and return nothing (no GFile to + * leak). file_ref() returns a fresh GFile handle for tests that need to keep a + * reference (to pass to the navigator, delete, or set attributes). */ + +static char * +make_temp_dir(void) { + GError *p_err = NULL; + char *c_dir = g_dir_make_tmp("ggaze-nav-XXXXXX", &p_err); + g_assert_no_error(p_err); + return (c_dir); +} + +/* Recursively remove a flat temp dir tree, then free the path string. */ +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)); + 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 +write_file(const char *c_dir, const char *c_name, const char *c_body, + gssize i_len) { + 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; + GOutputStream *p_out = (GOutputStream *)g_file_replace( + p_file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, &p_err); + g_assert_no_error(p_err); + if (i_len < 0) { + i_len = (gssize)strlen(c_body); + } + gsize u_written = 0; + g_assert_true(g_output_stream_write_all(p_out, c_body, (gsize)i_len, + &u_written, NULL, &p_err)); + g_assert_no_error(p_err); + g_assert_true(g_output_stream_close(p_out, NULL, &p_err)); + g_assert_no_error(p_err); + g_object_unref(p_out); + g_object_unref(p_file); + g_free(c_path); +} + +static void +touch_file(const char *c_dir, const char *c_name) { + write_file(c_dir, c_name, "x", 1); +} + +static GFile * +file_ref(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); + g_free(c_path); + return (p_file); /* (transfer full) */ +} + +/* Get the basename, assert it equals c_expected, and free it (LSan-clean). */ +static void +assert_name(GFile *p_file, const char *c_expected) { + char *c_name = g_file_get_basename(p_file); + g_assert_cmpstr(c_name, ==, c_expected); + g_free(c_name); +} + +/* pump the default main context for up to u_ms, returning TRUE if p_pred + * became TRUE. */ +static gboolean +pump_until(gboolean (*p_pred)(gpointer), gpointer p_data, guint u_ms) { + GMainContext *p_ctx = g_main_context_default(); + for (guint u = 0; u < u_ms; u++) { + if (p_pred != NULL && p_pred(p_data)) { + return (TRUE); + } + g_main_context_iteration(p_ctx, FALSE); + g_usleep(1000); /* 1 ms */ + } + return (p_pred != NULL && p_pred(p_data)); +} + +static gboolean +_count_is(gpointer p_data) { + return (navigator_get_count((Navigator *)p_data) == + GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(p_data), "want"))); +} + +/* --- tests --------------------------------------------------------------- */ + +static void +test_filter_and_listing(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "xx", 2); + write_file(c_dir, "b.png", "yyy", 3); + write_file(c_dir, "c.txt", "z", 1); /* excluded: not an image */ + write_file(c_dir, ".hidden.jpg", "h", 1); /* excluded: dotfile */ + write_file(c_dir, "d.gif", "yy", 2); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + g_assert_cmpint(navigator_get_count(p_nav), ==, 3); /* a, b, d */ + assert_name(navigator_get_file(p_nav, 0), "a.jpg"); + assert_name(navigator_get_file(p_nav, 1), "b.png"); + assert_name(navigator_get_file(p_nav, 2), "d.gif"); + + navigator_delete(p_nav); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_hide_raw_sidecars(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "img.jpg", "x", 1); + write_file(c_dir, "img.raf", "x", 1); /* RAW twin of img.jpg */ + write_file(c_dir, "lonely.nef", "x", 1); /* no JPEG twin -> kept */ + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + g_assert_cmpint(navigator_get_count(p_nav), ==, 2); /* img.jpg, lonely.nef */ + g_assert_true(navigator_get_hide_raw(p_nav)); + + navigator_set_hide_raw(p_nav, FALSE); + g_assert_cmpint(navigator_get_count(p_nav), ==, 3); /* all three */ + + navigator_delete(p_nav); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_sort_time_and_size(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "12345", 5); + write_file(c_dir, "b.jpg", "0123456789ABCDEF", 16); + GFile *p_small = file_ref(c_dir, "a.jpg"); + GFile *p_big = file_ref(c_dir, "b.jpg"); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + g_assert_cmpint(navigator_get_sort(p_nav), ==, GGAZE_SORT_NAME); + assert_name(navigator_get_current(p_nav), "a.jpg"); + + navigator_set_sort(p_nav, GGAZE_SORT_SIZE); + assert_name(navigator_get_file(p_nav, 0), "a.jpg"); /* size 5 < 16 */ + + /* Give a.jpg a newer mtime than b.jpg via a direct attribute set. */ + guint64 u_t_old = (guint64)1000000; + guint64 u_t_new = (guint64)9000000; + GError *p_seterr = NULL; + g_assert_true(g_file_set_attribute(p_big, "time::modified", + G_FILE_ATTRIBUTE_TYPE_UINT64, &u_t_old, + G_FILE_QUERY_INFO_NONE, NULL, &p_seterr)); + g_assert_no_error(p_seterr); + g_assert_true(g_file_set_attribute(p_small, "time::modified", + G_FILE_ATTRIBUTE_TYPE_UINT64, &u_t_new, + G_FILE_QUERY_INFO_NONE, NULL, &p_seterr)); + g_assert_no_error(p_seterr); + + navigator_set_sort(p_nav, GGAZE_SORT_TIME); + /* ascending: b.jpg (older) first, a.jpg (newer) last. */ + assert_name(navigator_get_file(p_nav, 0), "b.jpg"); + assert_name(navigator_get_file(p_nav, navigator_get_count(p_nav) - 1), + "a.jpg"); + + navigator_delete(p_nav); + g_object_unref(p_small); + g_object_unref(p_big); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_prev_next_wrap(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "x", 1); + write_file(c_dir, "b.jpg", "x", 1); + write_file(c_dir, "c.jpg", "x", 1); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 0); + + g_assert_true(navigator_next(p_nav)); + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 1); + g_assert_true(navigator_next(p_nav)); + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 2); + g_assert_true(navigator_next(p_nav)); /* wrap to 0 */ + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 0); + g_assert_true(navigator_prev(p_nav)); /* wrap to last */ + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 2); + + navigator_set_wrap(p_nav, FALSE); + g_assert_cmpint(navigator_get_wrap(p_nav), ==, FALSE); + navigator_last(p_nav); + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 2); + g_assert_false(navigator_next(p_nav)); /* no wrap */ + navigator_first(p_nav); + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 0); + g_assert_false(navigator_prev(p_nav)); /* no wrap */ + + navigator_delete(p_nav); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_set_current_file(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "x", 1); + write_file(c_dir, "b.jpg", "x", 1); + GFile *p_b = file_ref(c_dir, "b.jpg"); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 0); /* a.jpg */ + + g_assert_true(navigator_set_current_file(p_nav, p_b)); /* b.jpg at idx 1 */ + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 1); + assert_name(navigator_get_current(p_nav), "b.jpg"); + + navigator_delete(p_nav); + g_object_unref(p_b); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_marks(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "x", 1); + write_file(c_dir, "b.jpg", "x", 1); + write_file(c_dir, "c.jpg", "x", 1); + GFile *p_a = file_ref(c_dir, "a.jpg"); + GFile *p_b = file_ref(c_dir, "b.jpg"); + GFile *p_c = file_ref(c_dir, "c.jpg"); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 0); + navigator_toggle_mark(p_nav, p_b); + g_assert_true(navigator_is_marked(p_nav, p_b)); + g_assert_false(navigator_is_marked(p_nav, p_a)); + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 1); + navigator_toggle_mark(p_nav, p_b); /* untoggle */ + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 0); + + navigator_mark_range(p_nav, p_a, p_c); /* mark a, b, c */ + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 3); + + /* marks survive a re-sort */ + navigator_set_sort(p_nav, GGAZE_SORT_SIZE); + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 3); + g_assert_true(navigator_is_marked(p_nav, p_a)); + + GList *p_marks = navigator_get_marks(p_nav); + g_assert_cmpint(g_list_length(p_marks), ==, 3); + g_list_free_full(p_marks, (GDestroyNotify)g_object_unref); + + navigator_clear_marks(p_nav); + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 0); + + navigator_mark_all(p_nav); + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 3); + + navigator_delete(p_nav); + g_object_unref(p_a); + g_object_unref(p_b); + g_object_unref(p_c); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_rescan_and_nearest_fallback(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "x", 1); + write_file(c_dir, "b.jpg", "x", 1); + write_file(c_dir, "c.jpg", "x", 1); + GFile *p_b = file_ref(c_dir, "b.jpg"); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + navigator_set_current_file(p_nav, p_b); /* current = b.jpg (idx 1) */ + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 1); + + /* Delete b.jpg from disk and rescan: current gone -> nearest fallback. */ + g_assert_true(g_file_delete(p_b, NULL, NULL)); + navigator_rescan(p_nav); + g_assert_cmpint(navigator_get_count(p_nav), ==, 2); + /* nearest by position: old idx 1 -> stays at idx 1 (now c.jpg). */ + assert_name(navigator_get_current(p_nav), "c.jpg"); + + /* Adding a file via rescan keeps the current. */ + touch_file(c_dir, "d.jpg"); + navigator_rescan(p_nav); + g_assert_cmpint(navigator_get_count(p_nav), ==, 3); + assert_name(navigator_get_current(p_nav), "c.jpg"); + + navigator_delete(p_nav); + g_object_unref(p_b); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_remove_clears_mark_and_falls_back(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "x", 1); + write_file(c_dir, "b.jpg", "x", 1); + write_file(c_dir, "c.jpg", "x", 1); + GFile *p_b = file_ref(c_dir, "b.jpg"); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + navigator_set_current_file(p_nav, p_b); + navigator_mark_all(p_nav); + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 3); + + g_assert_true(navigator_remove(p_nav, p_b)); /* remove current */ + g_assert_cmpint(navigator_get_count(p_nav), ==, 2); + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, + 2); /* b's mark cleared */ + /* nearest fallback: was idx 1 -> now c.jpg at idx 1. */ + assert_name(navigator_get_current(p_nav), "c.jpg"); + + g_assert_false(navigator_remove(p_nav, p_b)); /* already gone */ + + navigator_delete(p_nav); + g_object_unref(p_b); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_monitor_add(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "x", 1); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + navigator_set_debounce_ms(p_nav, 5); /* fast for the test */ + g_assert_cmpint(navigator_get_count(p_nav), ==, 1); + + touch_file(c_dir, "b.jpg"); + g_object_set_data(G_OBJECT(p_nav), "want", GUINT_TO_POINTER(2)); + g_assert_true(pump_until(_count_is, p_nav, 2000)); + + navigator_delete(p_nav); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +on_changed_cb(Navigator *p_nav, gpointer p_data) { + (void)p_nav; + (*(gint *)p_data)++; +} + +static void +test_changed_signal(void) { + char *c_dir = make_temp_dir(); + touch_file(c_dir, "a.jpg"); + touch_file(c_dir, "b.jpg"); + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + gint i_count = 0; + g_signal_connect(p_nav, "changed", G_CALLBACK(on_changed_cb), &i_count); + + navigator_emit_changed(p_nav); + g_assert_cmpint(i_count, ==, 1); + navigator_next(p_nav); + g_assert_cmpint(i_count, ==, 2); + navigator_set_sort(p_nav, GGAZE_SORT_SIZE); + g_assert_cmpint(i_count, ==, 3); + navigator_clear_marks(p_nav); + g_assert_cmpint(i_count, ==, 4); + + navigator_delete(p_nav); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_empty_dir(void) { + char *c_dir = make_temp_dir(); + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + + g_assert_cmpint(navigator_get_count(p_nav), ==, 0); + g_assert_cmpint(navigator_get_current_index(p_nav), ==, -1); + g_assert_null(navigator_get_current(p_nav)); + g_assert_false(navigator_prev(p_nav)); + g_assert_false(navigator_next(p_nav)); + g_assert_false(navigator_first(p_nav)); + g_assert_false(navigator_last(p_nav)); + g_assert_cmpint(navigator_get_mark_count(p_nav), ==, 0); + g_assert_null(navigator_get_marks(p_nav)); + g_assert_cmpint(navigator_get_remaining(p_nav), ==, 0); + g_assert_true(g_file_equal(navigator_get_dir(p_nav), p_dirf)); + + navigator_delete(p_nav); + g_object_unref(p_dirf); + cleanup_temp_dir(c_dir); +} + +static void +test_remove_before_cursor(void) { + char *c_dir = make_temp_dir(); + write_file(c_dir, "a.jpg", "x", 1); + write_file(c_dir, "b.jpg", "x", 1); + write_file(c_dir, "c.jpg", "x", 1); + GFile *p_a = file_ref(c_dir, "a.jpg"); + GFile *p_c = file_ref(c_dir, "c.jpg"); + + GFile *p_dirf = g_file_new_for_path(c_dir); + Navigator *p_nav = navigator_new(p_dirf, GGAZE_SORT_NAME, TRUE, TRUE); + navigator_set_current_file(p_nav, p_c); /* current = c.jpg (idx 2) */ + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 2); + g_assert_true(navigator_remove(p_nav, p_a)); /* remove idx 0 */ + /* cursor decrements: c.jpg stays current, now at idx 1. */ + g_assert_cmpint(navigator_get_current_index(p_nav), ==, 1); + assert_name(navigator_get_current(p_nav), "c.jpg"); + + navigator_delete(p_nav); + g_object_unref(p_a); + g_object_unref(p_c); + 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("/navigator/filter_and_listing", test_filter_and_listing); + g_test_add_func("/navigator/hide_raw_sidecars", test_hide_raw_sidecars); + g_test_add_func("/navigator/sort_time_and_size", test_sort_time_and_size); + g_test_add_func("/navigator/prev_next_wrap", test_prev_next_wrap); + g_test_add_func("/navigator/set_current_file", test_set_current_file); + g_test_add_func("/navigator/marks", test_marks); + g_test_add_func("/navigator/rescan_fallback", + test_rescan_and_nearest_fallback); + g_test_add_func("/navigator/remove", test_remove_clears_mark_and_falls_back); + g_test_add_func("/navigator/remove_before_cursor", + test_remove_before_cursor); + g_test_add_func("/navigator/monitor_add", test_monitor_add); + g_test_add_func("/navigator/changed_signal", test_changed_signal); + g_test_add_func("/navigator/empty_dir", test_empty_dir); + return (g_test_run()); +}
\ No newline at end of file diff --git a/tests/test_walk_folder.c b/tests/test_walk_folder.c new file mode 100644 index 0000000..660b164 --- /dev/null +++ b/tests/test_walk_folder.c @@ -0,0 +1,196 @@ +/*:* + * ggaze โ walk-the-folder integration test + * + * Drives the real window wiring: open a file (its parent folder becomes the + * navigator), then walk prev/next/first/last and assert the viewer's texture + * dimensions update and the header carries "n/total". Uses committed fixtures + * copied into a temp dir; an optional ./sample-images variant skips when the + * corpus is absent. Needs a display (integration suite; CI uses xvfb). + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "viewer.h" +#include "window.h" + +#include <gdk/gdk.h> +#include <gio/gio.h> +#include <glib.h> +#include <gtk/gtk.h> +#include <string.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); +} + +/* A fresh GFile handle for a file created by copy_fixture (transfer full). */ +static GFile * +file_in(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); + g_free(c_path); + return (p_file); +} + +/* Recursively remove a flat temp dir tree, then free the path string. */ +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)); + 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 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 +assert_dims(GgazeWindow *p_win, int i_w, int i_h) { + GdkTexture *p_tex = viewer_texture(p_win); + g_assert_nonnull(p_tex); + g_assert_cmpint(gdk_texture_get_width(p_tex), ==, i_w); + g_assert_cmpint(gdk_texture_get_height(p_tex), ==, i_h); +} + +static void +assert_title_has(GgazeWindow *p_win, const char *c_substr) { + const gchar *c_title = gtk_window_get_title(GTK_WINDOW(p_win)); + g_assert_nonnull(c_title); + g_assert_nonnull(g_strstr_len(c_title, -1, c_substr)); +} + +/* Name sort: plain.jpg (6x3), rot6.jpg (8x4 orient6 -> 4x8), small.png (5x2). + */ +static void +test_walk_temp(void) { + GError *p_err = NULL; + char *c_dir = g_dir_make_tmp("ggaze-walk-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"); + GFile *p_plain = file_in(c_dir, "plain.jpg"); + + GgazeWindow *p_win = new_window(); + ggaze_window_open(p_win, p_plain); /* current = plain.jpg */ + assert_dims(p_win, 6, 3); + assert_title_has(p_win, "1/3"); + + ggaze_window_next(p_win); + assert_dims(p_win, 4, 8); /* rot6.jpg */ + assert_title_has(p_win, "2/3"); + + ggaze_window_next(p_win); + assert_dims(p_win, 5, 2); /* small.png */ + assert_title_has(p_win, "3/3"); + + ggaze_window_prev(p_win); + assert_dims(p_win, 4, 8); /* back to rot6.jpg */ + + ggaze_window_first(p_win); + assert_dims(p_win, 6, 3); + ggaze_window_last(p_win); + assert_dims(p_win, 5, 2); + + g_object_unref(p_win); + g_object_unref(p_plain); + cleanup_temp_dir(c_dir); +} + +static void +test_walk_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; + } + /* Pick the first JPEG. */ + GError *p_err = NULL; + GDir *p_d = g_dir_open(c_dir, 0, &p_err); + g_assert_no_error(p_err); + const gchar *c_name = NULL; + while ((c_name = g_dir_read_name(p_d)) != NULL) { + if (g_str_has_suffix(c_name, ".jpg") || + g_str_has_suffix(c_name, ".JPG") || + g_str_has_suffix(c_name, ".png")) { + break; + } + } + /* c_name is borrowed from p_d; build the path before closing the dir. */ + 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 in corpus"); + 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_assert_nonnull(viewer_texture(p_win)); + + /* Walk a few; each step must produce a (possibly new) texture. */ + for (int i = 0; i < 5; i++) { + ggaze_window_next(p_win); + g_assert_nonnull(viewer_texture(p_win)); + } + + g_object_unref(p_win); + g_free(c_path); +} + +int +main(int i_argc, char **c_argv) { + g_test_init(&i_argc, &c_argv, NULL); + /* Tolerate host GTK WARNINGs; keep CRITICALs fatal. */ + 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("/walk/temp", test_walk_temp); + g_test_add_func("/walk/sample", test_walk_sample); + return (g_test_run()); +}
\ No newline at end of file diff --git a/tests/test_window.c b/tests/test_window.c index 0f3fa74..63e54bb 100644 --- a/tests/test_window.c +++ b/tests/test_window.c @@ -50,15 +50,21 @@ test_stack_has_two_views(void) { static void test_open_titles_window(void) { - GgazeWindow *p_win = new_window(); - - GFile *p_file = g_file_new_for_path("/tmp/IMG_0001.jpg"); + const gchar *c_dir = g_getenv("GGAZE_FIXTURES_DIR"); + g_assert_nonnull(c_dir); + gchar *c_path = g_build_filename(c_dir, "plain.jpg", NULL); + GgazeWindow *p_win = new_window(); + GFile *p_file = g_file_new_for_path(c_path); ggaze_window_open(p_win, p_file); - - g_assert_cmpstr(gtk_window_get_title(GTK_WINDOW(p_win)), ==, "IMG_0001.jpg"); - + /* M2: opening a file lists its parent folder; the header title carries + * the current filename + "n/total". */ + const gchar *c_title = gtk_window_get_title(GTK_WINDOW(p_win)); + g_assert_nonnull(c_title); + g_assert_nonnull(g_strstr_len(c_title, -1, "plain.jpg")); + g_assert_nonnull(g_strstr_len(c_title, -1, "/")); g_object_unref(p_file); g_object_unref(p_win); + g_free(c_path); } int |
