summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-14 09:08:10 +0300
committerPaul Buetow <paul@buetow.org>2026-07-14 09:08:10 +0300
commit95858bc4a819c118cf76dd38d6da0365ead997a9 (patch)
tree739544e2314c3e0bab65fb9c75e2c17e89069017 /tests
parent12be1b9ae52829292d13cc2fcc6ff7571d050d62 (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.build24
-rw-r--r--tests/test_mover.c99
-rw-r--r--tests/test_opener.c85
-rw-r--r--tests/test_runner.c101
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