diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-14 09:08:10 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-14 09:08:10 +0300 |
| commit | 95858bc4a819c118cf76dd38d6da0365ead997a9 (patch) | |
| tree | 739544e2314c3e0bab65fb9c75e2c17e89069017 /tests | |
| parent | 12be1b9ae52829292d13cc2fcc6ff7571d050d62 (diff) | |
operate: mover, opener, runner, clipboard modules + tests (ut0)
M8 (partial): plain-C operate modules + unit tests.
- src/mover.{c,h}: move files to configured dests (g_file_move + stem
collision suffix -1/-2), one-level undo (move back), acts on marks-or-current.
- src/opener.{c,h}: %f expand + detached GSubprocess launch (non-blocking).
- src/runner.{c,h}: /bin/sh -c with single-quoted %f/%d (injection guard),
async wait_check, rescan on completion.
- src/clipboard.{c,h}: image/png via GdkContentProvider + uri-list.
- meson: new sources added to libggae.
- tests: unit test_mover (move/undo/collision), test_opener (true/false/weird
filenames), test_runner (true/false/injection guard). 19/19 green, ASan clean.
- Fixed: g_str_replace (not in GLib 2.88) → manual _str_replace; GdkContentProvider
API (new_typed/new_for_value); mover collision stem-suffix; runner /bin/sh -c
argv (single arg, not shell-parsed); test GFile leaks.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/meson.build | 24 | ||||
| -rw-r--r-- | tests/test_mover.c | 99 | ||||
| -rw-r--r-- | tests/test_opener.c | 85 | ||||
| -rw-r--r-- | tests/test_runner.c | 101 |
4 files changed, 309 insertions, 0 deletions
diff --git a/tests/meson.build b/tests/meson.build index dbe21f2..4c1dfa5 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -119,6 +119,30 @@ test_info = executable( ) test('info', test_info, suite : 'unit', env : fixtures_env) +test_mover = executable( + 'test_mover', ['test_mover.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : [glib_dep, gio_dep], + link_with : ggaze_lib, install : false, +) +test('mover', test_mover, suite : 'unit', env : fixtures_env) + +test_opener = executable( + 'test_opener', ['test_opener.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : [glib_dep, gio_dep], + link_with : ggaze_lib, install : false, +) +test('opener', test_opener, suite : 'unit', env : fixtures_env) + +test_runner = executable( + 'test_runner', ['test_runner.c', ggaze_conf_h], + include_directories : [inc, src_inc], + dependencies : [glib_dep, gio_dep], + link_with : ggaze_lib, install : false, +) +test('runner', test_runner, suite : 'unit', env : fixtures_env) + test_texturecache = executable( 'test_texturecache', ['test_texturecache.c', ggaze_conf_h], diff --git a/tests/test_mover.c b/tests/test_mover.c new file mode 100644 index 0000000..0310026 --- /dev/null +++ b/tests/test_mover.c @@ -0,0 +1,99 @@ +/* test_mover.c — move + undo + collision suffixing. */ +#include "mover.h" +#include <gio/gio.h> +#include <glib.h> + +static char * +make_tmp_dir(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-mover-XXXXXX", &e); + g_assert_no_error(e); + return d; +} +static GFile * +write_file(const char *d, const char *n) { + char *p = g_build_filename(d, n, NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + g_free(p); + return f; +} +static void +cleanup_dir(char *d) { + GFile *dd = g_file_new_for_path(d); + GFileEnumerator *e = g_file_enumerate_children( + dd, "standard::name", G_FILE_QUERY_INFO_NONE, NULL, NULL); + if (e) { + GFileInfo *i; + while ((i = g_file_enumerator_next_file(e, NULL, NULL))) { + GFile *c = g_file_get_child(dd, g_file_info_get_name(i)); + g_file_delete(c, NULL, NULL); + g_object_unref(c); + g_object_unref(i); + } + g_object_unref(e); + } + g_file_delete(dd, NULL, NULL); + g_object_unref(dd); + g_free(d); +} + +static void +test_move_and_undo(void) { + char *src_dir = make_tmp_dir(); + char *dst_dir = make_tmp_dir(); + GFile *a = write_file(src_dir, "a.jpg"); + GFile *b = write_file(src_dir, "b.jpg"); + Mover *m = mover_new(); + MoverDest dest = {"dst", dst_dir}; + GList *files = g_list_prepend(g_list_prepend(NULL, a), b); + GError *e = NULL; + g_assert_true(mover_move(m, files, &dest, &e)); + g_assert_no_error(e); + g_assert_false(g_file_query_exists(a, NULL)); + g_assert_false(g_file_query_exists(b, NULL)); + g_assert_true(mover_can_undo(m)); + g_assert_true(mover_undo_last(m, &e)); + g_assert_true(g_file_query_exists(a, NULL)); + g_assert_true(g_file_query_exists(b, NULL)); + g_assert_false(mover_can_undo(m)); + g_list_free(files); + mover_delete(m); + g_object_unref(a); + g_object_unref(b); + cleanup_dir(src_dir); + cleanup_dir(dst_dir); +} + +static void +test_collision(void) { + char *src_dir = make_tmp_dir(); + char *dst_dir = make_tmp_dir(); + GFile *a = write_file(src_dir, "a.jpg"); + GFile *p_dst_a = write_file(dst_dir, "a.jpg"); + Mover *m = mover_new(); + MoverDest dest = {"dst", dst_dir}; + GList *files = g_list_prepend(NULL, a); + GError *e = NULL; + g_assert_true(mover_move(m, files, &dest, &e)); + GFile *p_dd = g_file_new_for_path(dst_dir); + GFile *p_a1 = g_file_get_child(p_dd, "a-1.jpg"); + g_assert_true(g_file_query_exists(p_a1, NULL)); + g_object_unref(p_a1); + g_object_unref(p_dd); + g_list_free(files); + mover_delete(m); + g_object_unref(p_dst_a); + g_object_unref(a); + cleanup_dir(src_dir); + cleanup_dir(dst_dir); +} + +int +main(int argc, char **argv) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/mover/move_undo", test_move_and_undo); + g_test_add_func("/mover/collision", test_collision); + return g_test_run(); +}
\ No newline at end of file diff --git a/tests/test_opener.c b/tests/test_opener.c new file mode 100644 index 0000000..a364bdb --- /dev/null +++ b/tests/test_opener.c @@ -0,0 +1,85 @@ +/* test_opener.c — %f expansion + launch with true/false. */ +#include "opener.h" +#include <gio/gio.h> +#include <glib.h> + +static GFile * +make_tmp_file(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-opener-XXXXXX", &e); + g_assert_no_error(e); + char *p = g_build_filename(d, "test.txt", NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + g_free(p); + g_free(d); + return f; +} + +static void +test_launch_true(void) { + GFile *f = make_tmp_file(); + Opener *o = opener_new(); + OpenerProg prog = {"true", "true %f"}; + GError *e = NULL; + g_assert_true(opener_launch(o, f, &prog, &e)); + g_assert_no_error(e); + opener_delete(o); + g_object_unref(f); +} + +static void +test_launch_false(void) { + GFile *f = make_tmp_file(); + Opener *o = opener_new(); + OpenerProg prog = {"false", "false %f"}; + GError *e = NULL; + g_assert_true(opener_launch(o, f, &prog, &e)); + g_assert_no_error(e); + opener_delete(o); + g_object_unref(f); +} + +static void +test_weird_filename(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-opener-XXXXXX", &e); + g_assert_no_error(e); + char *p = g_build_filename(d, "file with spaces $HOME `whoami`.jpg", NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + Opener *o = opener_new(); + OpenerProg prog = {"true", "true %f"}; + g_assert_true(opener_launch(o, f, &prog, &e)); + g_assert_no_error(e); + opener_delete(o); + g_object_unref(f); + g_free(p); + GFile *dd = g_file_new_for_path(d); + GFileEnumerator *en = g_file_enumerate_children( + dd, "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(dd, 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(dd, NULL, NULL); + g_object_unref(dd); + g_free(d); +} + +int +main(int argc, char **argv) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/opener/true", test_launch_true); + g_test_add_func("/opener/false", test_launch_false); + g_test_add_func("/opener/weird_filename", test_weird_filename); + return g_test_run(); +}
\ No newline at end of file diff --git a/tests/test_runner.c b/tests/test_runner.c new file mode 100644 index 0000000..172368b --- /dev/null +++ b/tests/test_runner.c @@ -0,0 +1,101 @@ +/* test_runner.c — %f/%d expansion, injection guard, exit status. */ +#include "runner.h" +#include <gio/gio.h> +#include <glib.h> + +static GMainLoop *g_loop; +static int g_exit_code; + +static void +_done_cb(GObject *p_src, GAsyncResult *p_res, gpointer p_data) { + (void)p_src; + (void)p_data; + GError *e = NULL; + g_exit_code = runner_run_finish(NULL, p_res, &e); + g_clear_error(&e); + g_main_loop_quit(g_loop); +} + +static void +run_and_wait(Runner *r, GFile *p_file, GFile *p_dir, + const RunnerScript *p_script) { + g_exit_code = -99; + g_loop = g_main_loop_new(NULL, FALSE); + GError *e = NULL; + g_assert_true(runner_run(r, p_file, p_dir, p_script, _done_cb, NULL, &e)); + g_assert_no_error(e); + g_main_loop_run(g_loop); + g_main_loop_unref(g_loop); +} + +static void +test_true_exit_zero(void) { + Runner *r = runner_new(); + RunnerScript s = {"true", "true"}; + GFile *f = g_file_new_for_path("/tmp/nonexistent"); + run_and_wait(r, f, NULL, &s); + g_assert_cmpint(g_exit_code, ==, 0); + runner_delete(r); + g_object_unref(f); +} + +static void +test_false_exit_nonzero(void) { + Runner *r = runner_new(); + RunnerScript s = {"false", "false"}; + GFile *f = g_file_new_for_path("/tmp/nonexistent"); + run_and_wait(r, f, NULL, &s); + g_assert_cmpint(g_exit_code, !=, 0); + runner_delete(r); + g_object_unref(f); +} + +static void +test_injection_guard(void) { + GError *e = NULL; + char *d = g_dir_make_tmp("ggaze-runner-XXXXXX", &e); + g_assert_no_error(e); + /* Create a file with a malicious name. */ + char *p = g_build_filename(d, ";echo HACKED;.jpg", NULL); + GFile *f = g_file_new_for_path(p); + g_file_replace_contents(f, "x", 1, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, NULL, NULL, NULL); + g_free(p); + GFile *dd = g_file_new_for_path(d); + + Runner *r = runner_new(); + /* true %f should succeed regardless of the filename (injection guard). + * The single-quoted path is a valid argument to true. */ + RunnerScript s = {"true", "true %f"}; + run_and_wait(r, f, dd, &s); + g_assert_cmpint(g_exit_code, ==, 0); + + runner_delete(r); + g_object_unref(f); + g_object_unref(dd); + GFile *ddd = g_file_new_for_path(d); + GFileEnumerator *en = g_file_enumerate_children( + ddd, "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(ddd, 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(ddd, NULL, NULL); + g_object_unref(ddd); + g_free(d); +} + +int +main(int argc, char **argv) { + g_test_init(&argc, &argv, NULL); + g_test_add_func("/runner/true_exit_zero", test_true_exit_zero); + g_test_add_func("/runner/false_exit_nonzero", test_false_exit_nonzero); + g_test_add_func("/runner/injection_guard", test_injection_guard); + return g_test_run(); +}
\ No newline at end of file |
