summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/changeset.c589
-rw-r--r--tests/client.c246
-rw-r--r--tests/dbus.c544
-rw-r--r--tests/dconf-mock-dbus.c99
-rw-r--r--tests/dconf-mock-gvdb.c211
-rw-r--r--tests/dconf-mock-shm.c124
-rw-r--r--tests/dconf-mock.h37
-rw-r--r--tests/engine.c2100
-rw-r--r--tests/gvdb.c438
-rw-r--r--tests/gvdbs/empty_gvdbbin0 -> 32 bytes
-rw-r--r--tests/gvdbs/example_gvdbbin0 -> 246 bytes
-rw-r--r--tests/gvdbs/example_gvdb.big-endianbin0 -> 246 bytes
-rw-r--r--tests/gvdbs/file_empty0
-rw-r--r--tests/gvdbs/file_too_small1
-rw-r--r--tests/gvdbs/invalid_header1
-rw-r--r--tests/gvdbs/nested_gvdbbin0 -> 371 bytes
-rw-r--r--tests/meson.build61
-rw-r--r--tests/paths.c98
-rw-r--r--tests/profile/broken-profile6
-rw-r--r--tests/profile/colourful13
-rw-r--r--tests/profile/dos2
-rw-r--r--tests/profile/empty-profile0
-rw-r--r--tests/profile/gdm2
-rw-r--r--tests/profile/many-sources10
-rw-r--r--tests/profile/no-newline-longline1
-rw-r--r--tests/profile/test-profile1
-rw-r--r--tests/profile/will-never-exist1
-rw-r--r--tests/shm.c175
-rwxr-xr-xtests/test-dconf.py817
-rw-r--r--tests/tmpdir.c53
-rw-r--r--tests/tmpdir.h9
-rw-r--r--tests/writer.c233
32 files changed, 5872 insertions, 0 deletions
diff --git a/tests/changeset.c b/tests/changeset.c
new file mode 100644
index 0000000..5f046df
--- /dev/null
+++ b/tests/changeset.c
@@ -0,0 +1,589 @@
+#include "../common/dconf-changeset.h"
+
+static gboolean
+should_not_run (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static gboolean
+is_null (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ return value == NULL;
+}
+
+static gboolean
+is_not_null (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ return value != NULL;
+}
+
+static void
+test_basic (void)
+{
+ DConfChangeset *changeset;
+ gboolean result;
+ GVariant *value;
+ gint n_items;
+
+ changeset = dconf_changeset_new ();
+ dconf_changeset_ref (changeset);
+ dconf_changeset_all (changeset, should_not_run, NULL);
+ n_items = dconf_changeset_describe (changeset, NULL, NULL, NULL);
+ g_assert_cmpint (n_items, ==, 0);
+ dconf_changeset_unref (changeset);
+ dconf_changeset_unref (changeset);
+
+ changeset = dconf_changeset_new_write ("/value/a", NULL);
+ result = dconf_changeset_all (changeset, is_null, NULL);
+ g_assert (result);
+ result = dconf_changeset_all (changeset, is_not_null, NULL);
+ g_assert (!result);
+
+ result = dconf_changeset_get (changeset, "/value/a", &value);
+ g_assert (result);
+ g_assert (value == NULL);
+
+ result = dconf_changeset_get (changeset, "/value/b", &value);
+ g_assert (!result);
+
+ dconf_changeset_set (changeset, "/value/b", g_variant_new_int32 (123));
+ result = dconf_changeset_all (changeset, is_null, NULL);
+ g_assert (!result);
+ result = dconf_changeset_all (changeset, is_not_null, NULL);
+ g_assert (!result);
+
+ result = dconf_changeset_get (changeset, "/value/a", &value);
+ g_assert (result);
+ g_assert (value == NULL);
+
+ result = dconf_changeset_get (changeset, "/value/b", &value);
+ g_assert (result);
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 123);
+ g_variant_unref (value);
+
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_string ("a string"));
+ result = dconf_changeset_all (changeset, is_null, NULL);
+ g_assert (!result);
+ result = dconf_changeset_all (changeset, is_not_null, NULL);
+ g_assert (result);
+
+ result = dconf_changeset_get (changeset, "/value/a", &value);
+ g_assert (result);
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "a string");
+ g_variant_unref (value);
+
+ result = dconf_changeset_get (changeset, "/value/b", &value);
+ g_assert (result);
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 123);
+ g_variant_unref (value);
+
+ dconf_changeset_unref (changeset);
+}
+
+static void
+test_similarity (void)
+{
+ DConfChangeset *a, *b;
+
+ a = dconf_changeset_new ();
+ b = dconf_changeset_new ();
+
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_set (a, "/value/a", g_variant_new_int32 (0));
+ g_assert (!dconf_changeset_is_similar_to (a, b));
+ g_assert (!dconf_changeset_is_similar_to (b, a));
+
+ /* different values for the same key are still the same */
+ dconf_changeset_set (b, "/value/a", g_variant_new_int32 (1));
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ /* make sure even a NULL is counted as different */
+ dconf_changeset_set (a, "/value/b", NULL);
+ g_assert (!dconf_changeset_is_similar_to (a, b));
+ g_assert (!dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_set (b, "/value/b", NULL);
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ /* different types are still the same */
+ dconf_changeset_set (b, "/value/a", g_variant_new_uint32 (222));
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_set (a, "/value/c", NULL);
+ dconf_changeset_set (b, "/value/d", NULL);
+ g_assert (!dconf_changeset_is_similar_to (a, b));
+ g_assert (!dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+}
+
+static void
+test_describe (void)
+{
+ DConfChangeset *changeset;
+ const gchar * const *keys;
+ GVariant * const *values;
+ const gchar *prefix;
+ gint n_items;
+ gint i;
+
+ /* test zero items */
+ changeset = dconf_changeset_new ();
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 0);
+ dconf_changeset_unref (changeset);
+
+ /* test one NULL item */
+ changeset = dconf_changeset_new_write ("/value/a", NULL);
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 1);
+ g_assert_cmpstr (prefix, ==, "/value/a");
+ g_assert_cmpstr (keys[0], ==, "");
+ g_assert (keys[1] == NULL);
+ g_assert (values[0] == NULL);
+
+
+ /* Check again */
+ prefix = NULL;
+ keys = NULL;
+ values = NULL;
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 1);
+ g_assert_cmpstr (prefix, ==, "/value/a");
+ g_assert_cmpstr (keys[0], ==, "");
+ g_assert (keys[1] == NULL);
+ g_assert (values[0] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test one non-NULL item */
+ changeset = dconf_changeset_new_write ("/value/a", g_variant_new_int32 (55));
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 1);
+ g_assert_cmpstr (prefix, ==, "/value/a");
+ g_assert_cmpstr (keys[0], ==, "");
+ g_assert (keys[1] == NULL);
+ g_assert_cmpint (g_variant_get_int32 (values[0]), ==, 55);
+ dconf_changeset_unref (changeset);
+
+ /* test many items */
+ changeset = dconf_changeset_new ();
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "/test/value/%2d", i);
+
+ dconf_changeset_set (changeset, key, g_variant_new_int32 (i));
+ }
+
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, i);
+ g_assert_cmpstr (prefix, ==, "/test/value/");
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "%2d", i);
+
+ g_assert_cmpstr (keys[i], ==, key);
+ g_assert_cmpint (g_variant_get_int32 (values[i]), ==, i);
+ }
+ g_assert (keys[n_items] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test many items with common names */
+ changeset = dconf_changeset_new ();
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "/test/value/aaa%02d", i);
+
+ dconf_changeset_set (changeset, key, g_variant_new_int32 (i));
+ }
+
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, i);
+ g_assert_cmpstr (prefix, ==, "/test/value/");
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "aaa%02d", i);
+
+ g_assert_cmpstr (keys[i], ==, key);
+ g_assert_cmpint (g_variant_get_int32 (values[i]), ==, i);
+ }
+ g_assert (keys[n_items] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test several values in different directories */
+ changeset = dconf_changeset_new ();
+ dconf_changeset_set (changeset, "/value/reset/", NULL);
+ dconf_changeset_set (changeset, "/value/int/a", g_variant_new_int32 (123));
+ dconf_changeset_set (changeset, "/value/string", g_variant_new_string ("bar"));
+ dconf_changeset_set (changeset, "/value/string/a", g_variant_new_string ("foo"));
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 4);
+ g_assert_cmpstr (prefix, ==, "/value/");
+ g_assert_cmpstr (keys[0], ==, "int/a");
+ g_assert_cmpint (g_variant_get_int32 (values[0]), ==, 123);
+ g_assert_cmpstr (keys[1], ==, "reset/");
+ g_assert (values[1] == NULL);
+ g_assert_cmpstr (keys[2], ==, "string");
+ g_assert_cmpstr (g_variant_get_string (values[2], NULL), ==, "bar");
+ g_assert_cmpstr (keys[3], ==, "string/a");
+ g_assert_cmpstr (g_variant_get_string (values[3], NULL), ==, "foo");
+ g_assert (keys[4] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test a couple of values in very different directories */
+ changeset = dconf_changeset_new_write ("/a/deep/directory/", NULL);
+ dconf_changeset_set (changeset, "/another/deep/directory/", NULL);
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 2);
+ g_assert_cmpstr (prefix, ==, "/");
+ g_assert_cmpstr (keys[0], ==, "a/deep/directory/");
+ g_assert_cmpstr (keys[1], ==, "another/deep/directory/");
+ g_assert (keys[2] == NULL);
+ g_assert (values[0] == NULL);
+ g_assert (values[1] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* one more similar case, but with the first letter different */
+ changeset = dconf_changeset_new_write ("/deep/directory/", NULL);
+ dconf_changeset_set (changeset, "/another/deep/directory/", NULL);
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 2);
+ g_assert_cmpstr (prefix, ==, "/");
+ g_assert_cmpstr (keys[0], ==, "another/deep/directory/");
+ g_assert_cmpstr (keys[1], ==, "deep/directory/");
+ g_assert (keys[2] == NULL);
+ g_assert (values[0] == NULL);
+ g_assert (values[1] == NULL);
+ dconf_changeset_unref (changeset);
+}
+
+static void
+test_reset (void)
+{
+ DConfChangeset *changeset;
+ GVariant *value;
+
+ changeset = dconf_changeset_new ();
+ g_assert (!dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (!dconf_changeset_get (changeset, "/value/a", &value));
+ /* value was not set */
+
+ /* set a value */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ /* record the reset */
+ dconf_changeset_set (changeset, "/value/", NULL);
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value == NULL);
+
+ /* write it back */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ /* reset again */
+ dconf_changeset_set (changeset, "/value/", NULL);
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value == NULL);
+
+ /* write again */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ /* reset a different way */
+ dconf_changeset_set (changeset, "/value/a", NULL);
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value == NULL);
+
+ /* write last time */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ dconf_changeset_unref (changeset);
+}
+
+static gboolean
+has_same_value (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ DConfChangeset *other = user_data;
+ GVariant *other_value;
+ gboolean success;
+
+ success = dconf_changeset_get (other, key, &other_value);
+ g_assert (success);
+
+ if (value == NULL)
+ g_assert (other_value == NULL);
+ else
+ {
+ g_assert (g_variant_equal (value, other_value));
+ g_variant_unref (other_value);
+ }
+
+ return TRUE;
+}
+
+static void
+test_serialisation (DConfChangeset *changes)
+{
+ GVariant *serialised;
+ DConfChangeset *copy;
+
+ serialised = dconf_changeset_serialise (changes);
+ copy = dconf_changeset_deserialise (serialised);
+ g_variant_unref (serialised);
+
+ g_assert (dconf_changeset_is_similar_to (copy, changes));
+ g_assert (dconf_changeset_is_similar_to (changes, copy));
+ g_assert (dconf_changeset_all (copy, has_same_value, changes));
+ g_assert (dconf_changeset_all (changes, has_same_value, copy));
+
+ dconf_changeset_unref (copy);
+}
+
+static void
+test_serialiser (void)
+{
+ DConfChangeset *changeset;
+
+ changeset = dconf_changeset_new ();
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/some/value", g_variant_new_int32 (333));
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/other/value", NULL);
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/other/value", g_variant_new_int32 (55));
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/other/", NULL);
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/", NULL);
+ test_serialisation (changeset);
+
+ dconf_changeset_unref (changeset);
+}
+
+static void
+test_change (void)
+{
+ DConfChangeset *deltaa, *deltab;
+ DConfChangeset *dba, *dbb;
+
+ dba = dconf_changeset_new_database (NULL);
+ dbb = dconf_changeset_new_database (dba);
+ g_assert (dconf_changeset_is_empty (dbb));
+ dconf_changeset_unref (dbb);
+
+ deltaa = dconf_changeset_new ();
+ dconf_changeset_change (dba, deltaa);
+ g_assert (dconf_changeset_is_empty (dba));
+ dconf_changeset_unref (deltaa);
+
+ deltaa = dconf_changeset_new_write ("/some/value", NULL);
+ dconf_changeset_change (dba, deltaa);
+ g_assert (dconf_changeset_is_empty (dba));
+ dconf_changeset_unref (deltaa);
+
+ deltaa = dconf_changeset_new ();
+ deltab = dconf_changeset_new_write ("/some/value", g_variant_new_int32 (123));
+ dconf_changeset_change (deltaa, deltab);
+ g_assert (!dconf_changeset_is_empty (deltaa));
+ dconf_changeset_change (dba, deltab);
+ g_assert (!dconf_changeset_is_empty (dba));
+ dconf_changeset_unref (deltaa);
+ dconf_changeset_unref (deltab);
+
+ deltaa = dconf_changeset_new ();
+ deltab = dconf_changeset_new_write ("/other/value", g_variant_new_int32 (123));
+ dconf_changeset_change (deltaa, deltab);
+ g_assert (!dconf_changeset_is_empty (deltaa));
+ dconf_changeset_unref (deltab);
+ deltab = dconf_changeset_new_write ("/other/", NULL);
+ dconf_changeset_change (deltaa, deltab);
+ g_assert (!dconf_changeset_is_empty (deltaa));
+ dconf_changeset_change (dba, deltaa);
+ g_assert (!dconf_changeset_is_empty (dba));
+
+ dbb = dconf_changeset_new_database (dba);
+ g_assert (!dconf_changeset_is_empty (dbb));
+
+ dconf_changeset_set (dba, "/some/", NULL);
+
+ dconf_changeset_set (dba, "/other/value", g_variant_new_int32 (123));
+ g_assert (!dconf_changeset_is_empty (dba));
+ dconf_changeset_change (dba, deltaa);
+ g_assert (dconf_changeset_is_empty (dba));
+ g_assert (!dconf_changeset_is_empty (dbb));
+
+ dconf_changeset_unref (deltaa);
+ dconf_changeset_unref (deltab);
+ dconf_changeset_unref (dbb);
+ dconf_changeset_unref (dba);
+}
+
+static void
+assert_diff_change_invariant (DConfChangeset *from,
+ DConfChangeset *to)
+{
+ DConfChangeset *copy;
+ DConfChangeset *diff;
+
+ /* Verify this promise from the docs:
+ *
+ * Applying the returned changeset to @from using
+ * dconf_changeset_change() will result in the two changesets being
+ * equal.
+ */
+
+ copy = dconf_changeset_new_database (from);
+ diff = dconf_changeset_diff (from, to);
+ if (diff)
+ {
+ dconf_changeset_change (copy, diff);
+ dconf_changeset_unref (diff);
+ }
+
+ /* Make sure they are now equal */
+ diff = dconf_changeset_diff (copy, to);
+ g_assert (diff == NULL);
+
+ /* Why not try it the other way too? */
+ diff = dconf_changeset_diff (to, copy);
+ g_assert (diff == NULL);
+
+ dconf_changeset_unref (copy);
+}
+
+static gchar *
+create_random_key (void)
+{
+ GString *key;
+ gint i, n;
+
+ key = g_string_new (NULL);
+ n = g_test_rand_int_range (1, 5);
+ for (i = 0; i < n; i++)
+ {
+ gint j;
+
+ g_string_append_c (key, '/');
+ for (j = 0; j < 5; j++)
+ g_string_append_c (key, g_test_rand_int_range ('a', 'z' + 1));
+ }
+
+ return g_string_free (key, FALSE);
+}
+
+static GVariant *
+create_random_value (void)
+{
+ return g_variant_new_take_string (create_random_key ());
+}
+
+static DConfChangeset *
+create_random_db (void)
+{
+ DConfChangeset *set;
+ gint i, n;
+
+ set = dconf_changeset_new_database (NULL);
+ n = g_test_rand_int_range (0, 20);
+ for (i = 0; i < n; i++)
+ {
+ GVariant *value = create_random_value ();
+ gchar *key = create_random_key ();
+
+ dconf_changeset_set (set, key, value);
+ g_free (key);
+ }
+
+ return set;
+}
+
+static void
+test_diff (void)
+{
+ DConfChangeset *a, *b;
+ gint i;
+
+ /* Check diff between two empties */
+ a = dconf_changeset_new_database (NULL);
+ b = dconf_changeset_new_database (NULL);
+ assert_diff_change_invariant (a, b);
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+
+ /* Check diff between two non-empties that are equal */
+ a = create_random_db ();
+ b = dconf_changeset_new_database (a);
+ assert_diff_change_invariant (a, b);
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+
+ /* Check diff between two random databases that are probably unequal */
+ for (i = 0; i < 1000; i++)
+ {
+ a = create_random_db ();
+ b = create_random_db ();
+ assert_diff_change_invariant (a, b);
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/changeset/basic", test_basic);
+ g_test_add_func ("/changeset/similarity", test_similarity);
+ g_test_add_func ("/changeset/describe", test_describe);
+ g_test_add_func ("/changeset/reset", test_reset);
+ g_test_add_func ("/changeset/serialiser", test_serialiser);
+ g_test_add_func ("/changeset/change", test_change);
+ g_test_add_func ("/changeset/diff", test_diff);
+
+ return g_test_run ();
+}
diff --git a/tests/client.c b/tests/client.c
new file mode 100644
index 0000000..1773ed1
--- /dev/null
+++ b/tests/client.c
@@ -0,0 +1,246 @@
+#define _DEFAULT_SOURCE
+#include "../client/dconf-client.h"
+#include "../engine/dconf-engine.h"
+#include "dconf-mock.h"
+#include <string.h>
+#include <stdlib.h>
+
+static GThread *main_thread;
+
+static void
+test_lifecycle (void)
+{
+ DConfClient *client;
+ GWeakRef weak;
+
+ client = dconf_client_new ();
+ g_weak_ref_init (&weak, client);
+ g_object_unref (client);
+
+ g_assert (g_weak_ref_get (&weak) == NULL);
+ g_weak_ref_clear (&weak);
+}
+
+static gboolean changed_was_called;
+
+static void
+changed (DConfClient *client,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gpointer user_data)
+{
+ g_assert (g_thread_self () == main_thread);
+
+ changed_was_called = TRUE;
+}
+
+static void
+check_and_free (GVariant *to_check,
+ GVariant *expected)
+{
+ if (expected)
+ {
+ g_variant_ref_sink (expected);
+ g_assert (to_check);
+
+ g_assert (g_variant_equal (to_check, expected));
+ g_variant_unref (to_check);
+ g_variant_unref (expected);
+ }
+ else
+ g_assert (to_check == NULL);
+}
+
+static void
+queue_up_100_writes (DConfClient *client)
+{
+ gint i;
+
+ /* We send 100 writes, letting them pile up.
+ * At no time should there be more than one write on the wire.
+ */
+ for (i = 0; i < 100; i++)
+ {
+ changed_was_called = FALSE;
+ dconf_client_write_fast (client, "/test/value", g_variant_new_int32 (i), NULL);
+ g_assert (changed_was_called);
+
+ /* We should always see the most recently written value. */
+ check_and_free (dconf_client_read (client, "/test/value"), g_variant_new_int32 (i));
+ check_and_free (dconf_client_read_full (client, "/test/value", DCONF_READ_DEFAULT_VALUE, NULL), NULL);
+ }
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), ==, 1);
+}
+
+static void
+fail_one_call (void)
+{
+ DConfEngineCallHandle *handle;
+ GError *error;
+
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "--expected error from testcase--");
+ handle = g_queue_pop_head (&dconf_mock_dbus_outstanding_call_handles);
+ dconf_engine_call_handle_reply (handle, NULL, error);
+ g_error_free (error);
+}
+
+static GLogWriterOutput
+log_writer_cb (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ gsize i;
+
+ for (i = 0; i < n_fields; i++)
+ {
+ if (g_strcmp0 (fields[i].key, "MESSAGE") == 0 &&
+ strstr (fields[i].value, "--expected error from testcase--"))
+ return G_LOG_WRITER_HANDLED;
+ }
+
+ return G_LOG_WRITER_UNHANDLED;
+}
+
+static void
+test_fast (void)
+{
+ DConfClient *client;
+
+ g_log_set_writer_func (log_writer_cb, NULL, NULL);
+
+ client = dconf_client_new ();
+ g_signal_connect (client, "changed", G_CALLBACK (changed), NULL);
+
+ queue_up_100_writes (client);
+
+ /* Start indicating that the writes failed.
+ *
+ * Because of the pending-merge logic, we should only have had to fail two calls.
+ *
+ * Each time, we should see a change notify.
+ */
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), == , 1);
+
+ changed_was_called = FALSE;
+ fail_one_call ();
+ g_assert (changed_was_called);
+
+ /* For the first failure, we should continue to see the most recently written value (99) */
+ check_and_free (dconf_client_read (client, "/test/value"), g_variant_new_int32 (99));
+ check_and_free (dconf_client_read_full (client, "/test/value", DCONF_READ_DEFAULT_VALUE, NULL), NULL);
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), == , 1);
+
+ changed_was_called = FALSE;
+ fail_one_call ();
+ g_assert (changed_was_called);
+
+ /* Should read back now as NULL */
+ check_and_free (dconf_client_read (client, "/test/value"), NULL);
+ check_and_free (dconf_client_read_full (client, "/test/value", DCONF_READ_DEFAULT_VALUE, NULL), NULL);
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), == , 0);
+
+ /* Cleanup */
+ g_signal_handlers_disconnect_by_func (client, changed, NULL);
+ g_object_unref (client);
+}
+
+static gboolean changed_a, changed_b, changed_c;
+
+static void
+coalesce_changed (DConfClient *client,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gpointer user_data)
+{
+ changed_a = g_str_equal (prefix, "/test/a") || g_strv_contains (changes, "a");
+ changed_b = g_str_equal (prefix, "/test/b") || g_strv_contains (changes, "b");
+ changed_c = g_str_equal (prefix, "/test/c") || g_strv_contains (changes, "c");
+}
+
+static void
+test_coalesce (void)
+{
+ gint i, a, b, c;
+ gboolean should_change_a, should_change_b, should_change_c;
+ g_autoptr(DConfClient) client = NULL;
+
+ gint changes[][3] = {
+ {1, 0, 0},
+ {1, 1, 1},
+ {0, 1, 1},
+ {0, 0, 1},
+ {0, 0, 0},
+ {1, 0, 0},
+ {1, 0, 0},
+ };
+
+ client = dconf_client_new ();
+ g_signal_connect (client, "changed", G_CALLBACK (coalesce_changed), NULL);
+
+ a = b = c = 0;
+
+ for (i = 0; i != G_N_ELEMENTS (changes); ++i)
+ {
+ g_autoptr(DConfChangeset) changeset = NULL;
+
+ should_change_a = changes[i][0];
+ should_change_b = changes[i][1];
+ should_change_c = changes[i][2];
+
+ changeset = dconf_changeset_new ();
+
+ if (should_change_a)
+ dconf_changeset_set (changeset, "/test/a", g_variant_new_int32 (++a));
+ if (should_change_b)
+ dconf_changeset_set (changeset, "/test/b", g_variant_new_int32 (++b));
+ if (should_change_c)
+ dconf_changeset_set (changeset, "/test/c", g_variant_new_int32 (++c));
+
+ changed_a = changed_b = changed_c = FALSE;
+
+ g_assert_true (dconf_client_change_fast (client, changeset, NULL));
+
+ /* Notifications should be only about keys we have just written. */
+ g_assert_cmpint (should_change_a, ==, changed_a);
+ g_assert_cmpint (should_change_b, ==, changed_b);
+ g_assert_cmpint (should_change_c, ==, changed_c);
+
+ /* We should see value from the most recent write or NULL if we haven't written it yet. */
+ check_and_free (dconf_client_read (client, "/test/a"), a == 0 ? NULL : g_variant_new_int32 (a));
+ check_and_free (dconf_client_read (client, "/test/b"), b == 0 ? NULL : g_variant_new_int32 (b));
+ check_and_free (dconf_client_read (client, "/test/c"), c == 0 ? NULL : g_variant_new_int32 (c));
+ }
+
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "1"), NULL);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "2"), NULL);
+
+ /* There should be no more requests since all but first have been
+ * coalesced together. */
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Cleanup */
+ g_signal_handlers_disconnect_by_func (client, changed, NULL);
+}
+
+int
+main (int argc, char **argv)
+{
+ setenv ("DCONF_PROFILE", SRCDIR "/profile/will-never-exist", TRUE);
+
+ main_thread = g_thread_self ();
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/client/lifecycle", test_lifecycle);
+ g_test_add_func ("/client/basic-fast", test_fast);
+ g_test_add_func ("/client/coalesce", test_coalesce);
+
+ return g_test_run ();
+}
diff --git a/tests/dbus.c b/tests/dbus.c
new file mode 100644
index 0000000..032cb04
--- /dev/null
+++ b/tests/dbus.c
@@ -0,0 +1,544 @@
+#include <string.h>
+#include <glib.h>
+#include <stdlib.h>
+
+/* Test the DBus communicaton code.
+ */
+
+#include "../engine/dconf-engine.h"
+
+static gboolean okay_in_main;
+static GThread *main_thread;
+static GThread *dbus_thread;
+static GQueue async_call_success_queue;
+static GQueue async_call_error_queue;
+static GMutex async_call_queue_lock;
+static gboolean signal_was_received;
+
+static void
+wait_for_queue_to_empty (GQueue *queue)
+{
+ okay_in_main = TRUE;
+
+ while (TRUE)
+ {
+ gboolean is_empty;
+
+ g_mutex_lock (&async_call_queue_lock);
+ is_empty = g_queue_is_empty (queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ if (is_empty)
+ return;
+
+ g_main_context_iteration (NULL, TRUE);
+ }
+
+ okay_in_main = FALSE;
+}
+
+static gboolean
+just_wake (gpointer user_data)
+{
+ return G_SOURCE_REMOVE;
+}
+
+static void
+signal_if_queue_is_empty (GQueue *queue)
+{
+ gboolean is_empty;
+
+ g_mutex_lock (&async_call_queue_lock);
+ is_empty = g_queue_is_empty (queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ if (is_empty)
+ g_idle_add (just_wake, NULL);
+}
+
+const GVariantType *
+dconf_engine_call_handle_get_expected_type (DConfEngineCallHandle *handle)
+{
+ return (GVariantType *) handle;
+}
+
+void
+dconf_engine_call_handle_reply (DConfEngineCallHandle *handle,
+ GVariant *parameters,
+ const GError *error)
+{
+ DConfEngineCallHandle *expected_handle;
+
+ /* Ensure that messages are never delivered in the main thread except
+ * by way of a mainloop (ie: not during sync calls).
+ *
+ * It's okay if they are delivered in another thread at the same time
+ * as a sync call is happening in the main thread, though...
+ */
+ g_assert (g_thread_self () != main_thread || okay_in_main);
+
+ /* Make sure that we only ever receive D-Bus calls from a single
+ * thread.
+ */
+ if (!dbus_thread)
+ dbus_thread = g_thread_self ();
+ g_assert (g_thread_self () == dbus_thread);
+
+ /* This is the passing case. */
+ if (parameters != NULL)
+ {
+ g_mutex_lock (&async_call_queue_lock);
+ g_assert (g_queue_is_empty (&async_call_error_queue));
+ expected_handle = g_queue_pop_head (&async_call_success_queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ g_assert (parameters != NULL);
+ g_assert (error == NULL);
+ g_assert (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(s)")));
+
+ g_assert (expected_handle == handle);
+ g_variant_type_free ((GVariantType *) handle);
+
+ signal_if_queue_is_empty (&async_call_success_queue);
+ }
+ else
+ {
+ g_mutex_lock (&async_call_queue_lock);
+ g_assert (g_queue_is_empty (&async_call_success_queue));
+ expected_handle = g_queue_pop_head (&async_call_error_queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ g_assert (parameters == NULL);
+ g_assert (error != NULL);
+
+ g_assert (expected_handle == handle);
+ g_variant_type_free ((GVariantType *) handle);
+
+ signal_if_queue_is_empty (&async_call_error_queue);
+ }
+}
+
+void
+dconf_engine_handle_dbus_signal (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *signal_name,
+ GVariant *parameters)
+{
+ g_assert (g_thread_self () != main_thread || okay_in_main);
+
+ if (!dbus_thread)
+ dbus_thread = g_thread_self ();
+ g_assert (g_thread_self () == dbus_thread);
+
+ if (g_str_equal (signal_name, "TestSignal"))
+ {
+ GVariant *expected;
+
+ expected = g_variant_parse (NULL, "('1', ['2', '3'])", NULL, NULL, NULL);
+ g_assert (g_variant_equal (parameters, expected));
+ g_variant_unref (expected);
+
+ signal_was_received = TRUE;
+ g_idle_add (just_wake, NULL);
+ }
+}
+
+static void
+test_creation_error_sync_with_error (void)
+{
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Sync with 'error' */
+ if (g_test_subprocess ())
+ {
+ GError *error = NULL;
+ GVariant *reply;
+
+ g_setenv ("DBUS_SESSION_BUS_ADDRESS", "some nonsense", 1);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), &error);
+
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, "some nonsense"));
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+static void
+test_creation_error_sync_without_error (void)
+{
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Sync without 'error' */
+ if (g_test_subprocess ())
+ {
+ GVariant *reply;
+
+ g_setenv ("DBUS_SESSION_BUS_ADDRESS", "some nonsense", 1);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), NULL);
+
+ g_assert (reply == NULL);
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+static void
+test_creation_error_async (void)
+{
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Async */
+ if (g_test_subprocess ())
+ {
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+
+ g_setenv ("DBUS_SESSION_BUS_ADDRESS", "some nonsense", 1);
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_error_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), handle, &error);
+
+ /* This could either fail immediately or asynchronously, depending
+ * on how the backend is setup.
+ */
+ if (success)
+ {
+ g_assert_no_error (error);
+
+ wait_for_queue_to_empty (&async_call_error_queue);
+ }
+ else
+ g_assert (error != NULL);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+static void
+test_sync_call_success (void)
+{
+ GError *error = NULL;
+ gchar *session_id;
+ gchar *system_id;
+ GVariant *reply;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "ListNames",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), &error);
+
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(as)")));
+ g_variant_unref (reply);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(s)"), &error);
+
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(s)")));
+ g_variant_get (reply, "(s)", &session_id);
+ g_variant_unref (reply);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SYSTEM,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(s)"), &error);
+
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(s)")));
+ g_variant_get (reply, "(s)", &system_id);
+ g_variant_unref (reply);
+
+ /* Make sure we actually saw two separate buses */
+ g_assert_cmpstr (session_id, !=, system_id);
+ g_free (session_id);
+ g_free (system_id);
+}
+
+static void
+test_sync_call_error (void)
+{
+ GError *error = NULL;
+ GVariant *reply;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Test receiving errors from the other side */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("(s)", ""), G_VARIANT_TYPE_UNIT, &error);
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, "org.freedesktop.DBus.Error.InvalidArgs"));
+ g_clear_error (&error);
+
+ /* Test with 'ay' to make sure transmitting that works as well */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("(ay)", NULL), G_VARIANT_TYPE_UNIT, &error);
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, "org.freedesktop.DBus.Error.InvalidArgs"));
+ g_clear_error (&error);
+
+ /* Test reply type errors */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(u)"), &error);
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, " type "));
+ g_clear_error (&error);
+
+ /* Test two oddities:
+ *
+ * - first, the dbus-1 backend can't handle return types other than
+ * 's' and 'as', so we do a method call that will get something
+ * else in order that we can check that the failure is treated
+ * properly
+ *
+ * - next, we want to make sure that the filter function for
+ * gdbus-filter doesn't block incoming method calls
+ */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "RequestName",
+ g_variant_new_parsed ("('ca.desrt.dconf.testsuite', uint32 0)"),
+ G_VARIANT_TYPE ("(u)"), &error);
+ if (reply != NULL)
+ {
+ guint s;
+
+ /* It worked, so we must be on gdbus... */
+ g_assert_no_error (error);
+
+ g_variant_get (reply, "(u)", &s);
+ g_assert_cmpuint (s, ==, 1);
+ g_variant_unref (reply);
+
+ /* Ping ourselves... */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "ca.desrt.dconf.testsuite", "/", "org.freedesktop.DBus.Peer",
+ "Ping", g_variant_new ("()"), G_VARIANT_TYPE_UNIT, &error);
+ g_assert (reply != NULL);
+ g_assert_no_error (error);
+ g_variant_unref (reply);
+ }
+ else
+ {
+ /* Else, we're on dbus1...
+ *
+ * Check that the error was emitted correctly.
+ */
+ g_assert_cmpstr (error->message, ==, "unable to handle message type '(u)'");
+ g_clear_error (&error);
+ }
+}
+
+static void
+test_async_call_success (void)
+{
+ gint i;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ for (i = 0; i < 1000; i++)
+ {
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_success_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), handle, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ }
+
+ wait_for_queue_to_empty (&async_call_success_queue);
+}
+
+static void
+test_async_call_error (void)
+{
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_error_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("(s)", ""), handle, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ wait_for_queue_to_empty (&async_call_error_queue);
+}
+
+static void
+test_sync_during_async (void)
+{
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+ GVariant *reply;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_success_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), handle, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "ListNames",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), &error);
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_variant_unref (reply);
+
+ wait_for_queue_to_empty (&async_call_success_queue);
+}
+
+static gboolean
+did_not_receive_signal (gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static void
+test_signal_receipt (void)
+{
+ GError *error = NULL;
+ GVariant *reply;
+ gint status;
+ guint id;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "AddMatch",
+ g_variant_new ("(s)", "type='signal',interface='ca.desrt.dconf.Writer'"),
+ G_VARIANT_TYPE_UNIT, &error);
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_variant_unref (reply);
+
+ status = system ("gdbus emit --session "
+ "--object-path /ca/desrt/dconf/Writer/testcase "
+ "--signal ca.desrt.dconf.Writer.TestSignal "
+ "\"'1'\" \"['2', '3']\"");
+ g_assert_cmpint (status, ==, 0);
+
+ id = g_timeout_add (30000, did_not_receive_signal, NULL);
+ while (!signal_was_received)
+ g_main_context_iteration (NULL, FALSE);
+ g_source_remove (id);
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+
+ main_thread = g_thread_self ();
+
+ dconf_engine_dbus_init_for_testing ();
+
+ /* test_creation_error absolutely must come first */
+ if (!g_str_equal (DBUS_BACKEND, "/libdbus-1"))
+ {
+ g_test_add_func (DBUS_BACKEND "/creation/error/sync-with-error", test_creation_error_sync_with_error);
+ g_test_add_func (DBUS_BACKEND "/creation/error/sync-without-error", test_creation_error_sync_without_error);
+ g_test_add_func (DBUS_BACKEND "/creation/error/async", test_creation_error_async);
+ }
+
+ g_test_add_func (DBUS_BACKEND "/sync-call/success", test_sync_call_success);
+ g_test_add_func (DBUS_BACKEND "/sync-call/error", test_sync_call_error);
+ g_test_add_func (DBUS_BACKEND "/async-call/success", test_async_call_success);
+ g_test_add_func (DBUS_BACKEND "/async-call/error", test_async_call_error);
+ g_test_add_func (DBUS_BACKEND "/sync-call/during-async", test_sync_during_async);
+ g_test_add_func (DBUS_BACKEND "/signal/receipt", test_signal_receipt);
+
+ return g_test_run ();
+}
diff --git a/tests/dconf-mock-dbus.c b/tests/dconf-mock-dbus.c
new file mode 100644
index 0000000..2bafa30
--- /dev/null
+++ b/tests/dconf-mock-dbus.c
@@ -0,0 +1,99 @@
+/*
+ * Copyright © 2012 Canonical Limited
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "../engine/dconf-engine.h"
+#include "dconf-mock.h"
+
+GQueue dconf_mock_dbus_outstanding_call_handles;
+
+gboolean
+dconf_engine_dbus_call_async_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ DConfEngineCallHandle *handle,
+ GError **error)
+{
+ g_variant_ref_sink (parameters);
+ g_variant_unref (parameters);
+
+ g_queue_push_tail (&dconf_mock_dbus_outstanding_call_handles, handle);
+
+ return TRUE;
+}
+
+void
+dconf_mock_dbus_async_reply (GVariant *reply,
+ GError *error)
+{
+ DConfEngineCallHandle *handle;
+
+ g_assert (!g_queue_is_empty (&dconf_mock_dbus_outstanding_call_handles));
+ handle = g_queue_pop_head (&dconf_mock_dbus_outstanding_call_handles);
+
+ if (reply)
+ {
+ const GVariantType *expected_type;
+
+ expected_type = dconf_engine_call_handle_get_expected_type (handle);
+ g_assert (expected_type == NULL || g_variant_is_of_type (reply, expected_type));
+ g_variant_ref_sink (reply);
+ }
+
+ dconf_engine_call_handle_reply (handle, reply, error);
+
+ if (reply)
+ g_variant_unref (reply);
+}
+
+void
+dconf_mock_dbus_assert_no_async (void)
+{
+ g_assert (g_queue_is_empty (&dconf_mock_dbus_outstanding_call_handles));
+}
+
+DConfMockDBusSyncCallHandler dconf_mock_dbus_sync_call_handler;
+
+GVariant *
+dconf_engine_dbus_call_sync_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *reply_type,
+ GError **error)
+{
+ GVariant *reply;
+
+ g_assert (dconf_mock_dbus_sync_call_handler != NULL);
+
+ g_variant_ref_sink (parameters);
+
+ reply = (* dconf_mock_dbus_sync_call_handler) (bus_type, bus_name, object_path, interface_name,
+ method_name, parameters, reply_type, error);
+
+ g_variant_unref (parameters);
+
+ g_assert (reply != NULL || (error == NULL || *error != NULL));
+
+ return reply ? g_variant_take_ref (reply) : NULL;
+}
diff --git a/tests/dconf-mock-gvdb.c b/tests/dconf-mock-gvdb.c
new file mode 100644
index 0000000..4f58de1
--- /dev/null
+++ b/tests/dconf-mock-gvdb.c
@@ -0,0 +1,211 @@
+#include "../gvdb/gvdb-reader.h"
+#include "dconf-mock.h"
+
+/* The global dconf_mock_gvdb_tables hashtable is modified all the time
+ * so we need to hold the lock while we access it.
+ *
+ * The hashtables contained within it are never modified, however. They
+ * can be safely accessed without a lock.
+ */
+
+static GHashTable *dconf_mock_gvdb_tables;
+static GMutex dconf_mock_gvdb_lock;
+
+typedef struct
+{
+ GVariant *value;
+ GvdbTable *table;
+} DConfMockGvdbItem;
+
+struct _GvdbTable
+{
+ GHashTable *table;
+ gboolean is_valid;
+ gboolean top_level;
+ gint ref_count;
+};
+
+static void
+dconf_mock_gvdb_item_free (gpointer data)
+{
+ DConfMockGvdbItem *item = data;
+
+ if (item->value)
+ g_variant_unref (item->value);
+
+ if (item->table)
+ gvdb_table_free (item->table);
+
+ g_slice_free (DConfMockGvdbItem, item);
+}
+
+static void
+dconf_mock_gvdb_init (void)
+{
+ if (dconf_mock_gvdb_tables == NULL)
+ dconf_mock_gvdb_tables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) gvdb_table_free);
+}
+
+GvdbTable *
+dconf_mock_gvdb_table_new (void)
+{
+ GvdbTable *table;
+
+ table = g_slice_new (GvdbTable);
+ table->table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, dconf_mock_gvdb_item_free);
+ table->ref_count = 1;
+ table->is_valid = TRUE;
+
+ return table;
+}
+
+void
+dconf_mock_gvdb_table_insert (GvdbTable *table,
+ const gchar *name,
+ GVariant *value,
+ GvdbTable *subtable)
+{
+ DConfMockGvdbItem *item;
+
+ g_assert (value == NULL || subtable == NULL);
+
+ if (subtable)
+ subtable->top_level = FALSE;
+
+ item = g_slice_new (DConfMockGvdbItem);
+ item->value = value ? g_variant_ref_sink (value) : NULL;
+ item->table = subtable;
+
+ g_hash_table_insert (table->table, g_strdup (name), item);
+}
+
+void
+dconf_mock_gvdb_install (const gchar *filename,
+ GvdbTable *table)
+{
+ g_mutex_lock (&dconf_mock_gvdb_lock);
+ dconf_mock_gvdb_init ();
+
+ if (table)
+ {
+ table->top_level = TRUE;
+ g_hash_table_insert (dconf_mock_gvdb_tables, g_strdup (filename), table);
+ }
+ else
+ g_hash_table_remove (dconf_mock_gvdb_tables, filename);
+
+ g_mutex_unlock (&dconf_mock_gvdb_lock);
+}
+
+void
+gvdb_table_free (GvdbTable *table)
+{
+ if (g_atomic_int_dec_and_test (&table->ref_count))
+ {
+ g_hash_table_unref (table->table);
+ g_slice_free (GvdbTable, table);
+ }
+}
+
+GvdbTable *
+dconf_mock_gvdb_table_ref (GvdbTable *table)
+{
+ g_atomic_int_inc (&table->ref_count);
+
+ return table;
+}
+
+GvdbTable *
+gvdb_table_get_table (GvdbTable *table,
+ const gchar *key)
+{
+ DConfMockGvdbItem *item;
+ GvdbTable *subtable;
+
+ item = g_hash_table_lookup (table->table, key);
+
+ if (item && item->table)
+ subtable = dconf_mock_gvdb_table_ref (item->table);
+ else
+ subtable = NULL;
+
+ return subtable;
+}
+
+gboolean
+gvdb_table_has_value (GvdbTable *table,
+ const gchar *key)
+{
+ DConfMockGvdbItem *item;
+
+ item = g_hash_table_lookup (table->table, key);
+
+ return item && item->value;
+}
+
+GVariant *
+gvdb_table_get_value (GvdbTable *table,
+ const gchar *key)
+{
+ DConfMockGvdbItem *item;
+
+ item = g_hash_table_lookup (table->table, key);
+
+ return (item && item->value) ? g_variant_ref (item->value) : NULL;
+}
+
+gchar **
+gvdb_table_list (GvdbTable *table,
+ const gchar *key)
+{
+ const gchar * const result[] = { "value", NULL };
+
+ g_assert_cmpstr (key, ==, "/");
+
+ if (!gvdb_table_has_value (table, "/value"))
+ return NULL;
+
+ return g_strdupv ((gchar **) result);
+}
+
+gchar **
+gvdb_table_get_names (GvdbTable *table,
+ gint *length)
+{
+ if (length)
+ *length = 0;
+
+ return g_new0 (gchar *, 0 + 1);
+}
+
+GvdbTable *
+gvdb_table_new (const gchar *filename,
+ gboolean trusted,
+ GError **error)
+{
+ GvdbTable *table;
+
+ g_mutex_lock (&dconf_mock_gvdb_lock);
+ dconf_mock_gvdb_init ();
+ table = g_hash_table_lookup (dconf_mock_gvdb_tables, filename);
+ if (table)
+ dconf_mock_gvdb_table_ref (table);
+ g_mutex_unlock (&dconf_mock_gvdb_lock);
+
+ if (table == NULL)
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "this gvdb does not exist");
+
+ return table;
+}
+
+gboolean
+gvdb_table_is_valid (GvdbTable *table)
+{
+ return table->is_valid;
+}
+
+void
+dconf_mock_gvdb_table_invalidate (GvdbTable *table)
+{
+ table->is_valid = FALSE;
+}
diff --git a/tests/dconf-mock-shm.c b/tests/dconf-mock-shm.c
new file mode 100644
index 0000000..588667e
--- /dev/null
+++ b/tests/dconf-mock-shm.c
@@ -0,0 +1,124 @@
+#include "../shm/dconf-shm.h"
+
+#include "dconf-mock.h"
+
+typedef struct
+{
+ guint8 flagged;
+ gint ref_count;
+} DConfMockShm;
+
+static GHashTable *dconf_mock_shm_table;
+static GMutex dconf_mock_shm_lock;
+static GString *dconf_mock_shm_log;
+
+static void
+dconf_mock_shm_unref (gpointer data)
+{
+ DConfMockShm *shm = data;
+
+ if (g_atomic_int_dec_and_test (&shm->ref_count))
+ g_slice_free (DConfMockShm, shm);
+}
+
+static DConfMockShm *
+dconf_mock_shm_ref (DConfMockShm *shm)
+{
+ g_atomic_int_inc (&shm->ref_count);
+
+ return shm;
+}
+
+guint8 *
+dconf_shm_open (const gchar *name)
+{
+ DConfMockShm *shm;
+
+ g_mutex_lock (&dconf_mock_shm_lock);
+
+ if G_UNLIKELY (dconf_mock_shm_table == NULL)
+ {
+ dconf_mock_shm_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, dconf_mock_shm_unref);
+ dconf_mock_shm_log = g_string_new (NULL);
+ }
+
+ shm = g_hash_table_lookup (dconf_mock_shm_table, name);
+ if (shm == NULL)
+ {
+ shm = g_slice_new0 (DConfMockShm);
+ g_hash_table_insert (dconf_mock_shm_table, g_strdup (name), dconf_mock_shm_ref (shm));
+ }
+
+ /* before unlocking... */
+ dconf_mock_shm_ref (shm);
+
+ g_string_append_printf (dconf_mock_shm_log, "open %s;", name);
+
+ g_mutex_unlock (&dconf_mock_shm_lock);
+
+ return &shm->flagged;
+}
+
+void
+dconf_shm_close (guint8 *shm)
+{
+ if (shm)
+ {
+ g_mutex_lock (&dconf_mock_shm_lock);
+ g_string_append (dconf_mock_shm_log, "close;");
+ g_mutex_unlock (&dconf_mock_shm_lock);
+
+ dconf_mock_shm_unref (shm);
+ }
+}
+
+gint
+dconf_mock_shm_flag (const gchar *name)
+{
+ DConfMockShm *shm;
+ gint count = 0;
+
+ g_mutex_lock (&dconf_mock_shm_lock);
+ shm = g_hash_table_lookup (dconf_mock_shm_table, name);
+ if (shm)
+ {
+ shm->flagged = 1;
+ count = shm->ref_count;
+ g_hash_table_remove (dconf_mock_shm_table, name);
+ }
+ g_mutex_unlock (&dconf_mock_shm_lock);
+
+ return count;
+}
+
+void
+dconf_mock_shm_reset (void)
+{
+ g_mutex_lock (&dconf_mock_shm_lock);
+ if (dconf_mock_shm_table != NULL)
+ {
+ GHashTableIter iter;
+ gpointer value;
+
+ g_hash_table_iter_init (&iter, dconf_mock_shm_table);
+ while (g_hash_table_iter_next (&iter, NULL, &value))
+ {
+ DConfMockShm *shm = value;
+
+ g_assert_cmpint (shm->ref_count, ==, 1);
+ g_hash_table_iter_remove (&iter);
+ }
+
+ g_string_truncate (dconf_mock_shm_log, 0);
+ }
+ g_mutex_unlock (&dconf_mock_shm_lock);
+}
+
+void
+dconf_mock_shm_assert_log (const gchar *expected_log)
+{
+ g_mutex_lock (&dconf_mock_shm_lock);
+ g_assert_cmpstr (dconf_mock_shm_log->str, ==, expected_log);
+ g_string_truncate (dconf_mock_shm_log, 0);
+ g_mutex_unlock (&dconf_mock_shm_lock);
+}
diff --git a/tests/dconf-mock.h b/tests/dconf-mock.h
new file mode 100644
index 0000000..b3bdcba
--- /dev/null
+++ b/tests/dconf-mock.h
@@ -0,0 +1,37 @@
+#ifndef __dconf_mock_h__
+#define __dconf_mock_h__
+
+#include "../gvdb/gvdb-reader.h"
+#include <gio/gio.h>
+
+typedef GVariant * (* DConfMockDBusSyncCallHandler) (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error);
+
+extern DConfMockDBusSyncCallHandler dconf_mock_dbus_sync_call_handler;
+extern GQueue dconf_mock_dbus_outstanding_call_handles;
+
+void dconf_mock_dbus_async_reply (GVariant *reply,
+ GError *error);
+void dconf_mock_dbus_assert_no_async (void);
+
+void dconf_mock_shm_reset (void);
+gint dconf_mock_shm_flag (const gchar *name);
+void dconf_mock_shm_assert_log (const gchar *expected_log);
+
+GvdbTable * dconf_mock_gvdb_table_new (void);
+void dconf_mock_gvdb_table_insert (GvdbTable *table,
+ const gchar *name,
+ GVariant *value,
+ GvdbTable *subtable);
+void dconf_mock_gvdb_table_invalidate (GvdbTable *table);
+void dconf_mock_gvdb_install (const gchar *filename,
+ GvdbTable *table);
+GvdbTable * dconf_mock_gvdb_table_ref (GvdbTable *table);
+
+#endif
diff --git a/tests/engine.c b/tests/engine.c
new file mode 100644
index 0000000..fd2a348
--- /dev/null
+++ b/tests/engine.c
@@ -0,0 +1,2100 @@
+#define _GNU_SOURCE
+
+#include "../engine/dconf-engine.h"
+#include "../engine/dconf-engine-profile.h"
+#include "../engine/dconf-engine-mockable.h"
+#include "../common/dconf-enums.h"
+#include "dconf-mock.h"
+
+#include <glib/gstdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+#include <math.h>
+
+/* Interpose to catch fopen(SYSCONFDIR "/dconf/profile/user") */
+static const gchar *filename_to_replace;
+static const gchar *filename_to_replace_it_with;
+
+FILE *
+dconf_engine_fopen (const char *filename,
+ const char *mode)
+{
+
+ if (filename_to_replace && g_str_equal (filename, filename_to_replace))
+ {
+ /* Crash if this file was unexpectedly opened */
+ g_assert (filename_to_replace_it_with != NULL);
+ filename = filename_to_replace_it_with;
+ }
+
+ return fopen (filename, mode);
+}
+
+static void assert_no_messages (void);
+static void assert_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment);
+static void assert_maybe_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment);
+
+static GThread *main_thread;
+static GString *change_log;
+
+void
+dconf_engine_change_notify (DConfEngine *engine,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gboolean is_writability,
+ gpointer origin_tag,
+ gpointer user_data)
+{
+ gchar *joined;
+
+ if (!change_log)
+ return;
+
+ if (is_writability)
+ g_string_append (change_log, "w:");
+
+ joined = g_strjoinv (",", (gchar **) changes);
+ g_string_append_printf (change_log, "%s:%d:%s:%s;",
+ prefix, g_strv_length ((gchar **) changes), joined,
+ tag ? tag : "nil");
+ g_free (joined);
+}
+
+static void
+verify_and_free (DConfEngineSource **sources,
+ gint n_sources,
+ const gchar * const *expected_names,
+ gint n_expected)
+{
+ gint i;
+
+ g_assert_cmpint (n_sources, ==, n_expected);
+
+ g_assert ((sources == NULL) == (n_sources == 0));
+
+ for (i = 0; i < n_sources; i++)
+ {
+ g_assert_cmpstr (sources[i]->name, ==, expected_names[i]);
+ dconf_engine_source_free (sources[i]);
+ }
+
+ g_free (sources);
+}
+
+static void
+test_five_times (const gchar *filename,
+ gint n_expected,
+ ...)
+{
+ const gchar **expected_names;
+ DConfEngineSource **sources;
+ gint n_sources;
+ va_list ap;
+ gint i;
+
+ expected_names = g_new (const gchar *, n_expected);
+ va_start (ap, n_expected);
+ for (i = 0; i < n_expected; i++)
+ expected_names[i] = va_arg (ap, const gchar *);
+ va_end (ap);
+
+ /* first try by supplying the profile filename via the API */
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ g_assert (filename_to_replace == NULL);
+ sources = dconf_engine_profile_open (filename, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+
+ /* next try supplying it via the environment */
+ g_setenv ("DCONF_PROFILE", filename, TRUE);
+ g_assert (filename_to_replace == NULL);
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ g_unsetenv ("DCONF_PROFILE");
+
+ /* next try supplying a profile name via API and intercepting fopen */
+ filename_to_replace = SYSCONFDIR "/dconf/profile/myprofile";
+ filename_to_replace_it_with = filename;
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ sources = dconf_engine_profile_open ("myprofile", &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ filename_to_replace = NULL;
+
+ /* next try the same, via the environment */
+ g_setenv ("DCONF_PROFILE", "myprofile", TRUE);
+ filename_to_replace = SYSCONFDIR "/dconf/profile/myprofile";
+ filename_to_replace_it_with = filename;
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ g_unsetenv ("DCONF_PROFILE");
+ filename_to_replace = NULL;
+
+ /* next try to have dconf pick it up as the default user profile */
+ filename_to_replace = SYSCONFDIR "/dconf/profile/user";
+ filename_to_replace_it_with = filename;
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ filename_to_replace = NULL;
+
+ filename_to_replace_it_with = NULL;
+ g_free (expected_names);
+}
+
+typedef struct
+{
+ const gchar *profile_path;
+ const gchar *expected_stderr_pattern;
+} ProfileParserOpenData;
+
+static void
+test_profile_parser_errors (gconstpointer test_data)
+{
+ const ProfileParserOpenData *data = test_data;
+ DConfEngineSource **sources;
+ gint n_sources;
+
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ sources = dconf_engine_profile_open (data->profile_path, &n_sources);
+ g_assert_cmpint (n_sources, ==, 0);
+ g_assert (sources == NULL);
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr (data->expected_stderr_pattern);
+}
+
+static void
+test_profile_parser (void)
+{
+ DConfEngineSource **sources;
+ gint n_sources;
+
+ test_five_times (SRCDIR "/profile/empty-profile", 0);
+ test_five_times (SRCDIR "/profile/test-profile", 1, "test");
+ test_five_times (SRCDIR "/profile/colourful", 4,
+ "user",
+ "other",
+ "verylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongname",
+ "nonewline");
+ test_five_times (SRCDIR "/profile/dos", 2, "user", "site");
+ test_five_times (SRCDIR "/profile/no-newline-longline", 0);
+ test_five_times (SRCDIR "/profile/many-sources", 10,
+ "user", "local", "room", "floor", "building",
+ "site", "region", "division", "country", "global");
+
+ /* finally, test that we get the default profile if the user profile
+ * file cannot be located and we do not specify another profile.
+ */
+ filename_to_replace = SYSCONFDIR "/dconf/profile/user";
+ filename_to_replace_it_with = SRCDIR "/profile/this-file-does-not-exist";
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ filename_to_replace = NULL;
+ g_assert_cmpint (n_sources, ==, 1);
+ g_assert_cmpstr (sources[0]->name, ==, "user");
+ dconf_engine_source_free (sources[0]);
+ g_free (sources);
+
+ dconf_mock_shm_reset ();
+}
+
+static gpointer
+test_signal_threadsafety_worker (gpointer user_data)
+{
+ gint *finished = user_data;
+ gint i;
+
+ for (i = 0; i < 20000; i++)
+ {
+ DConfEngine *engine;
+
+ engine = dconf_engine_new (NULL, NULL, NULL);
+ dconf_engine_unref (engine);
+ }
+
+ g_atomic_int_inc (finished);
+
+ return NULL;
+}
+
+static void
+test_signal_threadsafety (void)
+{
+#define N_WORKERS 4
+ GVariant *parameters;
+ gint finished = 0;
+ gint i;
+
+ parameters = g_variant_new_parsed ("('/test/key', [''], 'tag')");
+ g_variant_ref_sink (parameters);
+
+ for (i = 0; i < N_WORKERS; i++)
+ g_thread_unref (g_thread_new ("testcase worker", test_signal_threadsafety_worker, &finished));
+
+ while (g_atomic_int_get (&finished) < N_WORKERS)
+ dconf_engine_handle_dbus_signal (G_BUS_TYPE_SESSION,
+ ":1.2.3",
+ "/ca/desrt/dconf/Writer/user",
+ "Notify", parameters);
+ g_variant_unref (parameters);
+
+ dconf_mock_shm_reset ();
+}
+
+static void
+test_user_source (void)
+{
+ DConfEngineSource *source;
+ GvdbTable *table;
+ GvdbTable *locks;
+ gboolean reopened;
+
+ /* Create the source from a clean slate */
+ source = dconf_engine_source_new ("user-db:user");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+
+ /* Refresh it the first time.
+ * This should cause it to open the shm.
+ * FALSE should be returned because there is no database file.
+ */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ dconf_mock_shm_assert_log ("open user;");
+
+ /* Try to refresh it. There must be no IO at this point. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ dconf_mock_shm_assert_log ("");
+
+ /* Add a real database. */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/values/int32", g_variant_new_int32 (123456), NULL);
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+
+ /* Try to refresh it again.
+ * Because we didn't flag the change there must still be no IO.
+ */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ dconf_mock_shm_assert_log ("");
+
+ /* Now flag it and reopen. */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks == NULL);
+ g_assert (gvdb_table_has_value (source->values, "/values/int32"));
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* Do it again -- should get the same result, after some IO */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks == NULL);
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* "Delete" the gvdb and make sure dconf notices after a flag */
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", NULL);
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* Add a gvdb with a lock */
+ table = dconf_mock_gvdb_table_new ();
+ locks = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/values/int32", g_variant_new_int32 (123456), NULL);
+ dconf_mock_gvdb_table_insert (locks, "/values/int32", g_variant_new_boolean (TRUE), NULL);
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+
+ /* Reopen and check if we have the lock */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks != NULL);
+ g_assert (gvdb_table_has_value (source->values, "/values/int32"));
+ g_assert (gvdb_table_has_value (source->locks, "/values/int32"));
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* Reopen one last time */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks != NULL);
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ dconf_engine_source_free (source);
+ dconf_mock_shm_assert_log ("close;");
+
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", NULL);
+ dconf_mock_shm_reset ();
+}
+
+static void
+test_file_source (void)
+{
+ DConfEngineSource *source;
+ gboolean reopened;
+ GvdbTable *table;
+ GVariant *value;
+
+ source = dconf_engine_source_new ("file-db:/path/to/db");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ dconf_engine_source_free (source);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "unable to open file '/path/to/db'");
+
+ source = dconf_engine_source_new ("file-db:/path/to/db");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/value", g_variant_new_string ("first file"), NULL);
+ dconf_mock_gvdb_install ("/path/to/db", table);
+
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values);
+ g_assert (source->locks == NULL);
+ value = gvdb_table_get_value (source->values, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "first file");
+ g_variant_unref (value);
+
+ /* Of course this should do nothing... */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ /* Invalidate and replace */
+ dconf_mock_gvdb_table_invalidate (table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/value", g_variant_new_string ("second file"), NULL);
+ dconf_mock_gvdb_install ("/path/to/db", table);
+
+ /* Even when invalidated, this should still do nothing... */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ value = gvdb_table_get_value (source->values, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "first file");
+ g_variant_unref (value);
+
+ dconf_mock_gvdb_install ("/path/to/db", NULL);
+ dconf_engine_source_free (source);
+}
+
+
+static gboolean service_db_created;
+static GvdbTable *service_db_table;
+
+static GVariant *
+handle_service_request (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error)
+{
+ g_assert_cmpstr (bus_name, ==, "ca.desrt.dconf");
+ g_assert_cmpstr (interface_name, ==, "ca.desrt.dconf.Writer");
+ g_assert_cmpstr (method_name, ==, "Init");
+ g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "()");
+
+ if (g_str_equal (object_path, "/ca/desrt/dconf/shm/nil"))
+ {
+ service_db_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (service_db_table, "/values/int32", g_variant_new_int32 (123456), NULL);
+ dconf_mock_gvdb_install ("/RUNTIME/dconf-service/shm/nil", service_db_table);
+
+ /* Make sure this only happens the first time... */
+ g_assert (!service_db_created);
+ service_db_created = TRUE;
+
+ return g_variant_new ("()");
+ }
+ else
+ {
+ g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "Unknown DB type");
+ return NULL;
+ }
+}
+
+static void
+test_service_source (void)
+{
+ DConfEngineSource *source;
+ gboolean reopened;
+
+ /* Make sure we deal with errors from the service sensibly */
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ source = dconf_engine_source_new ("service-db:unknown/nil");
+ dconf_mock_dbus_sync_call_handler = handle_service_request;
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ reopened = dconf_engine_source_refresh (source);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*WARNING*: unable to open file*unknown/nil*expect degraded performance*");
+
+ /* Set up one that will work */
+ source = dconf_engine_source_new ("service-db:shm/nil");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+
+ /* Refresh it the first time.
+ *
+ * This should cause the service to be asked to create it.
+ *
+ * This should return TRUE because we just opened it.
+ */
+ dconf_mock_dbus_sync_call_handler = handle_service_request;
+ reopened = dconf_engine_source_refresh (source);
+ dconf_mock_dbus_sync_call_handler = NULL;
+ g_assert (service_db_created);
+ g_assert (reopened);
+
+ /* After that, a refresh should be a no-op. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ /* Close it and reopen it, ensuring that we don't hit the service
+ * again (because the file already exists).
+ *
+ * Note: dconf_mock_dbus_sync_call_handler = NULL, so D-Bus calls will
+ * assert.
+ */
+ dconf_engine_source_free (source);
+ source = dconf_engine_source_new ("service-db:shm/nil");
+ g_assert (source != NULL);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+
+ /* Make sure it has the content we expect to see */
+ g_assert (gvdb_table_has_value (source->values, "/values/int32"));
+
+ /* Now invalidate it and replace it with an empty one */
+ dconf_mock_gvdb_table_invalidate (service_db_table);
+ service_db_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/RUNTIME/dconf-service/shm/nil", service_db_table);
+
+ /* Now reopening should get the new one */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+
+ /* ...and we should find it to be empty */
+ g_assert (!gvdb_table_has_value (source->values, "/values/int32"));
+
+ /* We're done. */
+ dconf_engine_source_free (source);
+
+ /* This should not have done any shm... */
+ dconf_mock_shm_assert_log ("");
+
+ dconf_mock_gvdb_install ("/RUNTIME/dconf-service/shm/nil", NULL);
+ service_db_table = NULL;
+}
+
+static void
+test_system_source (void)
+{
+ DConfEngineSource *source;
+ GvdbTable *first_table;
+ GvdbTable *next_table;
+ gboolean reopened;
+
+ source = dconf_engine_source_new ("system-db:site");
+ g_assert (source != NULL);
+
+ /* Check to see that we get the warning about the missing file. */
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ /* Failing to open should return FALSE from refresh */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == NULL);
+
+ /* Attempt the reopen to make sure we don't get two warnings.
+ * We should see FALSE again since we go from NULL to NULL.
+ */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ /* Create the file after the fact and make sure it opens properly */
+ first_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", first_table);
+
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+
+ dconf_engine_source_free (source);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ /* Check that we only saw the warning, but only one time. */
+ g_test_trap_assert_stderr ("*this gvdb does not exist; expect degraded performance*");
+ g_test_trap_assert_stderr_unmatched ("*degraded*degraded*");
+
+ /* Create the file before the first refresh attempt */
+ first_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", first_table);
+ /* Hang on to a copy for ourselves for below... */
+ dconf_mock_gvdb_table_ref (first_table);
+
+ /* See that we get the database. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values == first_table);
+
+ /* Do a refresh, make sure there is no change. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == first_table);
+
+ /* Replace the table on "disk" but don't invalidate the old one */
+ next_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", next_table);
+
+ /* Make sure the old table remains open (ie: no IO performed) */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == first_table);
+
+ /* Now mark the first table invalid and reopen */
+ dconf_mock_gvdb_table_invalidate (first_table);
+ gvdb_table_free (first_table);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values == next_table);
+
+ /* Remove the file entirely and do the same thing */
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", NULL);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ dconf_engine_source_free (source);
+}
+
+static void
+invalidate_state (guint n_sources,
+ guint source_types,
+ gpointer *state)
+{
+ gint i;
+
+ for (i = 0; i < n_sources; i++)
+ if (source_types & (1u << i))
+ {
+ if (state[i])
+ {
+ dconf_mock_gvdb_table_invalidate (state[i]);
+ gvdb_table_free (state[i]);
+ }
+ }
+ else
+ {
+ dconf_mock_shm_flag (state[i]);
+ g_free (state[i]);
+ }
+}
+
+static void
+setup_state (guint n_sources,
+ guint source_types,
+ guint database_state,
+ gpointer *state)
+{
+ gint i;
+
+ for (i = 0; i < n_sources; i++)
+ {
+ guint contents = database_state % 7;
+ GvdbTable *table = NULL;
+ gchar *filename;
+
+ if (contents)
+ {
+ table = dconf_mock_gvdb_table_new ();
+
+ /* Even numbers get the value setup */
+ if ((contents & 1) == 0)
+ dconf_mock_gvdb_table_insert (table, "/value", g_variant_new_uint32 (i), NULL);
+
+ /* Numbers above 2 get the locks table */
+ if (contents > 2)
+ {
+ GvdbTable *locks;
+
+ locks = dconf_mock_gvdb_table_new ();
+
+ /* Numbers above 4 get the lock set */
+ if (contents > 4)
+ dconf_mock_gvdb_table_insert (locks, "/value", g_variant_new_boolean (TRUE), NULL);
+
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ }
+ }
+
+ if (source_types & (1u << i))
+ {
+ if (state)
+ {
+ if (table)
+ state[i] = dconf_mock_gvdb_table_ref (table);
+ else
+ state[i] = NULL;
+ }
+
+ filename = g_strdup_printf (SYSCONFDIR "/dconf/db/db%d", i);
+ }
+ else
+ {
+ if (state)
+ state[i] = g_strdup_printf ("db%d", i);
+
+ filename = g_strdup_printf ("/HOME/.config/dconf/db%d", i);
+ }
+
+ dconf_mock_gvdb_install (filename, table);
+ g_free (filename);
+
+ database_state /= 7;
+ }
+}
+
+static void
+create_profile (const gchar *filename,
+ guint n_sources,
+ guint source_types)
+{
+ GError *error = NULL;
+ GString *profile;
+ gint i;
+
+ profile = g_string_new (NULL);
+ for (i = 0; i < n_sources; i++)
+ if (source_types & (1u << i))
+ g_string_append_printf (profile, "system-db:db%d\n", i);
+ else
+ g_string_append_printf (profile, "user-db:db%d\n", i);
+ g_file_set_contents (filename, profile->str, profile->len, &error);
+ g_assert_no_error (error);
+ g_string_free (profile, TRUE);
+}
+
+static GQueue read_through_queues[12];
+
+static void
+check_read (DConfEngine *engine,
+ guint n_sources,
+ guint source_types,
+ guint database_state)
+{
+ gboolean any_values = FALSE;
+ gboolean any_locks = FALSE;
+ guint first_contents;
+ gint underlying = -1;
+ gint expected = -1;
+ gboolean writable;
+ GVariant *value;
+ gchar **list;
+ guint i;
+ gint n;
+
+ /* The value we expect to read is number of the first source that has
+ * the value set (ie: odd digit in database_state) up to the lowest
+ * level lock.
+ *
+ * We go over each database. If 'expected' has not yet been set and
+ * we find that we should have a value in this database, we set it.
+ * If we find that we should have a lock in this database, we unset
+ * any previous values (since they should not have been written).
+ *
+ * We intentionally code this loop in a different way than the one in
+ * dconf itself is currently implemented...
+ *
+ * We also take note of if we saw any locks and cross-check that with
+ * dconf_engine_is_writable(). We check if we saw and values at all
+ * and cross-check that with dconf_engine_list() (which ignores
+ * locks).
+ */
+ first_contents = database_state % 7;
+ for (i = 0; i < n_sources; i++)
+ {
+ guint contents = database_state % 7;
+
+ /* A lock here should prevent higher reads */
+ if (contents > 4)
+ {
+ /* Locks in the first database don't count... */
+ if (i != 0)
+ any_locks = TRUE;
+ expected = -1;
+ }
+
+ /* A value here should be read */
+ if (contents && !(contents & 1))
+ {
+ if (i != 0 && underlying == -1)
+ underlying = i;
+
+ if (expected == -1)
+ {
+ any_values = TRUE;
+ expected = i;
+ }
+ }
+
+ database_state /= 7;
+ }
+
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+
+ if (expected != -1)
+ {
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, expected);
+ g_variant_unref (value);
+ }
+ else
+ g_assert (value == NULL);
+
+ /* We are writable if the first database is a user database and we
+ * didn't encounter any locks...
+ */
+ writable = dconf_engine_is_writable (engine, "/value");
+ g_assert_cmpint (writable, ==, n_sources && !(source_types & 1) && !any_locks);
+
+ /* Check various read-through scenarios. Read-through should only be
+ * effective if the database is writable.
+ */
+ for (i = 0; i < G_N_ELEMENTS (read_through_queues); i++)
+ {
+ gint our_expected = expected;
+
+ if (writable)
+ {
+ /* If writable, see what our changeset did.
+ *
+ * 0: nothing
+ * 1: reset value (should see underlying value)
+ * 2: set value to 123
+ */
+ if ((i % 3) == 1)
+ our_expected = underlying;
+ else if ((i % 3) == 2)
+ our_expected = 123;
+ }
+
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, &read_through_queues[i], "/value");
+
+ if (our_expected != -1)
+ {
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, our_expected);
+ g_variant_unref (value);
+ }
+ else
+ g_assert (value == NULL);
+ }
+
+ /* Check listing */
+ g_strfreev (dconf_engine_list (engine, "/", &n));
+ list = dconf_engine_list (engine, "/", NULL);
+ g_assert_cmpint (g_strv_length (list), ==, n);
+ if (any_values)
+ {
+ g_assert_cmpstr (list[0], ==, "value");
+ g_assert (list[1] == NULL);
+ }
+ else
+ g_assert (list[0] == NULL);
+ g_strfreev (list);
+
+ /* Check the user value.
+ *
+ * This should be set only in the case that the first database is a
+ * user database (ie: writable) and the contents of that database are
+ * set (ie: 2, 4 or 6). See the table in the comment below.
+ *
+ * Note: we do not consider locks.
+ */
+ value = dconf_engine_read (engine, DCONF_READ_USER_VALUE, NULL, "/value");
+ if (value)
+ {
+ g_assert (first_contents && !(first_contents & 1) && !(source_types & 1));
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, 0);
+ g_variant_unref (value);
+ }
+ else
+ {
+ /* Three possibilities for failure:
+ * - first db did not exist
+ * - value was missing from first db
+ * - first DB was system-db
+ */
+ g_assert (!first_contents || (first_contents & 1) || (source_types & 1));
+ }
+
+ /* Check read_through vs. user-value */
+ for (i = 0; i < G_N_ELEMENTS (read_through_queues); i++)
+ {
+ /* It is only possible here to see one of three possibilities:
+ *
+ * - NULL
+ * - 0 (value from user's DB)
+ * - 123 (value from queue)
+ *
+ * We see these values regardless of writability. We do however
+ * ensure that we have a writable database as the first one.
+ */
+ value = dconf_engine_read (engine, DCONF_READ_USER_VALUE, &read_through_queues[i], "/value");
+
+ /* If we have no first source, or the first source is non-user
+ * than we should always do nothing (since we can't queue changes
+ * against a system db or one that doesn't exist).
+ */
+ if (n_sources == 0 || (source_types & 1) || (i % 3) == 0)
+ {
+ /* Changeset did nothing, so it should be same as above. */
+ if (value)
+ {
+ g_assert (first_contents && !(first_contents & 1) && !(source_types & 1));
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, 0);
+ }
+ else
+ g_assert (!first_contents || (first_contents & 1) || (source_types & 1));
+ }
+ else if ((i % 3) == 1)
+ {
+ /* Changeset did a reset, so we should always see NULL */
+ g_assert (value == NULL);
+ }
+ else if ((i % 3) == 2)
+ {
+ /* Changeset set a value, so we should see it */
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, 123);
+ }
+
+ if (value)
+ g_variant_unref (value);
+ }
+}
+
+static void
+test_read (void)
+{
+#define MAX_N_SOURCES 2
+ gpointer state[MAX_N_SOURCES];
+ gchar *profile_filename;
+ GError *error = NULL;
+ DConfEngine *engine;
+ guint i, j, k;
+ guint n;
+
+ /* This test throws a lot of messages about missing databases.
+ * Capture and ignore them.
+ */
+
+ /* Our test strategy is as follows:
+ *
+ * We only test a single key name. It is assumed that gvdb is working
+ * properly already so we are only interested in interactions between
+ * multiple databases for a given key name.
+ *
+ * The outermost loop is over 'n'. This is how many sources are in
+ * our test. We test 0 to 3 (which should be enough to cover all
+ * 'interesting' possibilities). 4 takes too long to run (2*7*7 ~=
+ * 100 times as long as 3).
+ *
+ * The next loop is over 'i'. This goes from 0 to 2^n - 1, with each
+ * bit deciding the type of source of the i-th element
+ *
+ * 0: user
+ *
+ * 1: system
+ *
+ * The next loop is over 'j'. This goes from 0 to 7^n - 1, with each
+ * base-7 digit deciding the state of the database file associated
+ * with the i-th source:
+ *
+ * j file has value has ".locks" has lock
+ * ----------------------------------------------------
+ * 0 0 - - -
+ * 1 1 0 0 -
+ * 2 1 1 0 -
+ * 3 1 0 1 0
+ * 4 1 1 1 0
+ * 5 1 0 1 1
+ * 6 1 1 1 1
+ *
+ * Where 'file' is if the database file exists, 'has value' is if a
+ * value exists at '/value' within the file, 'has ".locks"' is if
+ * there is a ".locks" subtable and 'has lock' is if there is a lock
+ * for '/value' within that table.
+ *
+ * Finally, we loop over 'k' as a state to transition to ('k' works
+ * the same way as 'j').
+ *
+ * Once we know 'n' and 'i', we can write a profile file.
+ *
+ * Once we know 'j' we can setup the initial state, create the engine
+ * and check that we got the expected value. Then we transition to
+ * state 'k' and make sure everything still works as expected.
+ *
+ * Since we want to test all j->k transitions, we do the initial setup
+ * of the engine (according to j) inside of the 'k' loop, since we
+ * need to test all possible transitions from 'j'.
+ *
+ * We additionally test the effect of read-through queues in 4
+ * situations:
+ *
+ * - NULL: no queue
+ * - 0: queue with no effect
+ * - 1: queue that resets the value
+ * - 2: queue that sets the value to 123
+ *
+ * For the cases (0, 1, 2) we can have multiple types of queue that
+ * achieve the desired effect. We can put more than 3 items in
+ * read_through_queues -- the expected behaviour is dictated by the
+ * value of (i % 3) where i is the array index.
+ */
+ {
+ /* We use a scheme to set up each queue. Again, we assume that
+ * GHashTable is working OK, so we only bother having "/value" as a
+ * changeset item (or not).
+ *
+ * We have an array of strings, each string defining the
+ * configuration of one queue. In each string, each character
+ * represents the contents of a changeset within the queue, in
+ * order.
+ *
+ * ' ' - empty changeset
+ * 's' - set value to 123
+ * 'r' - reset value
+ * 'x' - set value to 321
+ */
+ const gchar *queue_configs[] = {
+ "", "r", "s",
+ " ", "rr", "ss",
+ " ", "rs", "sr",
+ " ", "rx", "sx"
+ };
+ gint i;
+
+ G_STATIC_ASSERT (G_N_ELEMENTS (queue_configs) == G_N_ELEMENTS (read_through_queues));
+ for (i = 0; i < G_N_ELEMENTS (read_through_queues); i++)
+ {
+ const gchar *conf = queue_configs[i];
+ gint j;
+
+ for (j = 0; conf[j]; j++)
+ {
+ DConfChangeset *changeset;
+
+ changeset = dconf_changeset_new ();
+
+ switch (conf[j])
+ {
+ case ' ':
+ break;
+ case 'r':
+ dconf_changeset_set (changeset, "/value", NULL);
+ break;
+ case 's':
+ dconf_changeset_set (changeset, "/value", g_variant_new_uint32 (123));
+ break;
+ case 'x':
+ dconf_changeset_set (changeset, "/value", g_variant_new_uint32 (321));
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ g_queue_push_head (&read_through_queues[i], changeset);
+ }
+ }
+ }
+
+ /* We need a place to put the profile files we use for this test */
+ close (g_file_open_tmp ("dconf-testcase.XXXXXX", &profile_filename, &error));
+ g_assert_no_error (error);
+
+ for (n = 0; n <= MAX_N_SOURCES; n++)
+ for (i = 0; i < pow (2, n); i++)
+ {
+ gint n_possible_states = pow (7, n);
+
+ /* Step 1: write out the profile file */
+ create_profile (profile_filename, n, i);
+
+ for (j = 0; j < n_possible_states; j++)
+ for (k = 0; k < n_possible_states; k++)
+ {
+ guint64 old_state, new_state;
+
+ /* Step 2: setup the state */
+ setup_state (n, i, j, (j != k) ? state : NULL);
+
+ /* Step 3: create the engine */
+ engine = dconf_engine_new (profile_filename, NULL, NULL);
+
+ /* Step 4: read, and check result */
+ check_read (engine, n, i, j);
+ old_state = dconf_engine_get_state (engine);
+
+ /* Step 5: change to the new state */
+ if (j != k)
+ {
+ setup_state (n, i, k, NULL);
+ invalidate_state (n, i, state);
+ }
+
+ /* Step 6: read, and check result */
+ check_read (engine, n, i, k);
+ new_state = dconf_engine_get_state (engine);
+
+ g_assert ((j == k) == (new_state == old_state));
+
+ /* Clean up */
+ setup_state (n, i, 0, NULL);
+ dconf_engine_unref (engine);
+
+ assert_maybe_pop_message ("dconf", G_LOG_LEVEL_WARNING,
+ "unable to open file '" SYSCONFDIR "/dconf/db");
+ }
+ }
+
+ /* Clean up the tempfile we were using... */
+ g_unlink (profile_filename);
+ g_free (profile_filename);
+ dconf_mock_shm_reset ();
+
+ assert_no_messages ();
+}
+
+static void
+test_watch_fast (void)
+{
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+ guint64 a, b, c;
+
+ change_log = g_string_new (NULL);
+
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Check that establishing a watch works properly in the normal case.
+ */
+ a = dconf_engine_get_state (engine);
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ /* watches do not count as outstanding changes */
+ g_assert (!dconf_engine_has_outstanding (engine));
+ dconf_engine_sync (engine);
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, ==, b);
+ /* both AddMatch results come back before shm is flagged */
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+ dconf_mock_shm_flag ("user");
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, !=, b);
+ g_assert_cmpstr (change_log->str, ==, "");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Establish a watch and fail the race. */
+ a = dconf_engine_get_state (engine);
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ g_assert (!dconf_engine_has_outstanding (engine));
+ dconf_engine_sync (engine);
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, ==, b);
+ /* one AddMatch result comes back -after- shm is flagged */
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_shm_flag ("user");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, !=, b);
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+ /* Try to establish a watch again for the same path */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ g_assert (!dconf_engine_has_outstanding (engine));
+ dconf_engine_sync (engine);
+ c = dconf_engine_get_state (engine);
+ g_assert_cmpuint (b, ==, c);
+ /* The watch result was not sent, because the path was already watched */
+ dconf_mock_dbus_assert_no_async ();
+ c = dconf_engine_get_state (engine);
+ g_assert_cmpuint (b, ==, c);
+ /* Since the path was already being watched,
+ * do not expect a second false change notification */
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ /* nothing was done, because there is still a subscription left */
+ dconf_mock_dbus_assert_no_async ();
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", NULL);
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", NULL);
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static void
+test_watch_fast_simultaneous_subscriptions (void)
+{
+ /**
+ * Test that creating multiple subscriptions to the same path
+ * simultaneously (before receiving replies from D-Bus) only results in
+ * a single D-Bus match rule, and that it is removed at the right time.
+ */
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+
+ /* Set up */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+
+ /* Subscribe to the same path 3 times. Both AddMatch results succeed
+ * (one for each source). There is only one for each source*path.
+ */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Unsubscribe twice, after the AddMatch succeeds. There is still one
+ * active subscription, so no RemoveMatch request is sent. */
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Unsubscribe once more. The number of active subscriptions falls to 0
+ * and the D-Bus match rule is removed */
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* The shm was not flagged at any point - so no change notifications
+ * should not have been sent */
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ /* Clean up */
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static void
+test_watch_fast_successive_subscriptions (void)
+{
+ /**
+ * Test that subscribing to the same path multiple times successively
+ * (after waiting for any expected replies from D-Bus) results in only
+ * a single D-Bus match rule being created, and that it is created and
+ * destroyed at the right times.
+ */
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+
+ /* Set up */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Subscribe to a path, and simulate a change to the database while the
+ * AddMatch request is in progress */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_mock_shm_flag ("user");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+
+ /* When the AddMatch requests succeeds, expect a change notification
+ * for the path */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+
+ /* Subscribe to a path twice again, and simulate a change to the
+ * database */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_mock_shm_flag ("user");
+
+ /* There was already a match rule in place, so there should be no D-Bus
+ * requests and no change notifications */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+
+ /* Unsubscribe */
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_assert_no_async ();
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+
+ /* Clean up */
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static void
+test_watch_fast_short_lived_subscriptions (void)
+{
+ /**
+ * Test that subscribing and then immediately unsubscribing (without
+ * waiting for replies from D-Bus) multiple times to the same path
+ * correctly triggers D-Bus requests and change notifications in cases
+ * where the D-Bus match rule was not in place when the database was
+ * changed.
+ */
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+
+ /* Set up */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Subscribe to a path twice, and simulate a change to the database
+ * while the AddMatch request is in progress */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_mock_shm_flag ("user");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* When the AddMatch requests succeed, expect a change notification
+ * to have been sent for the path, even though the client has since
+ * unsubscribed. */
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+
+
+ /* Clean up */
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static const gchar *match_request_type;
+static gboolean got_match_request[5];
+
+static GVariant *
+handle_match_request (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error)
+{
+ const gchar *match_rule;
+
+ g_assert_cmpstr (bus_name, ==, "org.freedesktop.DBus");
+ /* any object path works... */
+ g_assert_cmpstr (interface_name, ==, "org.freedesktop.DBus");
+ g_assert_cmpstr (method_name, ==, match_request_type);
+ g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(s)");
+ g_variant_get (parameters, "(&s)", &match_rule);
+ g_assert (strstr (match_rule, "arg0path='/a/b/c'"));
+ g_assert (!got_match_request[bus_type]);
+ got_match_request[bus_type] = TRUE;
+
+ return g_variant_new ("()");
+}
+
+static void
+test_watch_sync (void)
+{
+ DConfEngine *engine;
+
+ dconf_mock_dbus_sync_call_handler = handle_match_request;
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ match_request_type = "AddMatch";
+
+ /* A match rule should be added when the first subscription is established */
+ dconf_engine_watch_sync (engine, "/a/b/c");
+ g_assert (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert (got_match_request[G_BUS_TYPE_SYSTEM]);
+ got_match_request[G_BUS_TYPE_SESSION] = FALSE;
+ got_match_request[G_BUS_TYPE_SYSTEM] = FALSE;
+
+ /* The match rule is now already in place, so more are not needed */
+ dconf_engine_watch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ dconf_engine_watch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ match_request_type = "RemoveMatch";
+
+ /* There are 3 subscriptions, so removing 2 should not remove
+ * the match rule */
+ dconf_engine_unwatch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ dconf_engine_unwatch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ /* The match rule should be removed when the last subscription is
+ * removed */
+ dconf_engine_unwatch_sync (engine, "/a/b/c");
+ g_assert (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert (got_match_request[G_BUS_TYPE_SYSTEM]);
+ got_match_request[G_BUS_TYPE_SESSION] = FALSE;
+ got_match_request[G_BUS_TYPE_SYSTEM] = FALSE;
+
+ dconf_engine_unref (engine);
+
+ dconf_mock_dbus_sync_call_handler = NULL;
+ match_request_type = NULL;
+}
+
+static void
+test_change_fast (void)
+{
+ DConfChangeset *empty, *good_write, *bad_write, *very_good_write, *slightly_bad_write;
+ GvdbTable *table, *locks;
+ DConfEngine *engine;
+ gboolean success;
+ GError *error = NULL;
+ GVariant *value;
+
+ change_log = g_string_new (NULL);
+
+ table = dconf_mock_gvdb_table_new ();
+ locks = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (locks, "/locked", g_variant_new_boolean (TRUE), NULL);
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ empty = dconf_changeset_new ();
+ good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ very_good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ dconf_changeset_set (very_good_write, "/to-reset", NULL);
+ slightly_bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ dconf_changeset_set (slightly_bad_write, "/to-reset", NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ success = dconf_engine_change_fast (engine, empty, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ success = dconf_engine_change_fast (engine, empty, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ success = dconf_engine_change_fast (engine, bad_write, NULL, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ success = dconf_engine_change_fast (engine, slightly_bad_write, NULL, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ /* Up to now, no D-Bus traffic should have been sent at all because we
+ * only had trivial and non-writable attempts.
+ *
+ * Now try some working cases
+ */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ success = dconf_engine_change_fast (engine, good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ /* That should have emitted a synthetic change event */
+ g_assert_cmpstr (change_log->str, ==, "/value:1::nil;");
+ g_string_set_size (change_log, 0);
+
+ /* Verify that the value is set */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+
+ /* Fail the attempted write. This should cause a warning and a change. */
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (NULL, error);
+ g_clear_error (&error);
+ g_assert_cmpstr (change_log->str, ==, "/value:1::nil;");
+ g_string_set_size (change_log, 0);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "failed to commit changes to dconf: something failed");
+
+ /* Verify that the value became unset due to the failure */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+
+ /* Now try a successful write */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ success = dconf_engine_change_fast (engine, good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ /* That should have emitted a synthetic change event */
+ g_assert_cmpstr (change_log->str, ==, "/value:1::nil;");
+ g_string_set_size (change_log, 0);
+
+ /* Verify that the value is set */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+
+ /* ACK the write. */
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag"), NULL);
+ g_clear_error (&error);
+ /* No change this time, since we already did it. */
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ /* Verify that the value became unset due to the in-flight queue
+ * clearing... */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+
+ /* Do that all again for a changeset with more than one item */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+ success = dconf_engine_change_fast (engine, very_good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpstr (change_log->str, ==, "/:2:to-reset,value:nil;");
+ g_string_set_size (change_log, 0);
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (NULL, error);
+ g_clear_error (&error);
+ g_assert_cmpstr (change_log->str, ==, "/:2:to-reset,value:nil;");
+ g_string_set_size (change_log, 0);
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "failed to commit changes to dconf: something failed");
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+ success = dconf_engine_change_fast (engine, very_good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpstr (change_log->str, ==, "/:2:to-reset,value:nil;");
+ g_string_set_size (change_log, 0);
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag"), NULL);
+ g_clear_error (&error);
+ g_assert_cmpstr (change_log->str, ==, "");
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+
+ dconf_engine_unref (engine);
+
+ dconf_changeset_unref (empty);
+ dconf_changeset_unref (good_write);
+ dconf_changeset_unref (very_good_write);
+ dconf_changeset_unref (bad_write);
+ dconf_changeset_unref (slightly_bad_write);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+}
+
+static GError *change_sync_error;
+static GVariant *change_sync_result;
+
+static GVariant *
+handle_write_request (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error)
+{
+ g_assert_cmpstr (bus_name, ==, "ca.desrt.dconf");
+ g_assert_cmpstr (interface_name, ==, "ca.desrt.dconf.Writer");
+
+ /* Assume that the engine can format the method call properly, but
+ * test that it can properly handle weird replies.
+ */
+
+ *error = change_sync_error;
+ return change_sync_result;
+}
+
+
+static void
+test_change_sync (void)
+{
+ DConfChangeset *empty, *good_write, *bad_write, *very_good_write, *slightly_bad_write;
+ GvdbTable *table, *locks;
+ DConfEngine *engine;
+ gboolean success;
+ GError *error = NULL;
+ gchar *tag;
+
+ table = dconf_mock_gvdb_table_new ();
+ locks = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (locks, "/locked", g_variant_new_boolean (TRUE), NULL);
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ empty = dconf_changeset_new ();
+ good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ very_good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ dconf_changeset_set (very_good_write, "/to-reset", NULL);
+ slightly_bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ dconf_changeset_set (slightly_bad_write, "/to-reset", NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ success = dconf_engine_change_sync (engine, empty, &tag, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_free (tag);
+
+ success = dconf_engine_change_sync (engine, empty, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ success = dconf_engine_change_sync (engine, bad_write, &tag, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ success = dconf_engine_change_sync (engine, slightly_bad_write, NULL, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ /* Up to now, no D-Bus traffic should have been sent at all because we
+ * only had trivial and non-writable attempts.
+ *
+ * Now try some working cases
+ */
+ dconf_mock_dbus_sync_call_handler = handle_write_request;
+ change_sync_result = g_variant_new ("(s)", "mytag");
+
+ success = dconf_engine_change_sync (engine, good_write, &tag, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpstr (tag, ==, "mytag");
+ g_free (tag);
+ change_sync_result = NULL;
+
+ change_sync_error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ success = dconf_engine_change_sync (engine, very_good_write, &tag, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT);
+ g_assert (!success);
+ g_clear_error (&error);
+ change_sync_error = NULL;
+
+ dconf_changeset_unref (empty);
+ dconf_changeset_unref (good_write);
+ dconf_changeset_unref (very_good_write);
+ dconf_changeset_unref (bad_write);
+ dconf_changeset_unref (slightly_bad_write);
+ dconf_engine_unref (engine);
+}
+
+static void
+send_signal (GBusType type,
+ const gchar *name,
+ const gchar *path,
+ const gchar *signame,
+ const gchar *args)
+{
+ GVariant *value;
+
+ value = g_variant_ref_sink (g_variant_new_parsed (args));
+ dconf_engine_handle_dbus_signal (type, name, path, signame, value);
+ g_variant_unref (value);
+}
+
+static void
+test_signals (void)
+{
+ DConfEngine *engine;
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Throw some non-sense at it to make sure it gets rejected */
+
+ /* Invalid signal name */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "UnNotify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "UnNotify", "('/', [''], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Bad path */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/use", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/use", "WritabilityNotify", "('/',)");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/sit", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/sit", "WritabilityNotify", "('/',)");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Wrong signature for signal */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/', [''], '')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/',)");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "WritabilityNotify", "('/', [''], '')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Signal delivered on wrong bus type */
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/site", "WritabilityNotify", "('/',)");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Empty changeset */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', @as [], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a/', @as [], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/a', @as [], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/a/', @as [], 'tag')");
+ /* Try to notify on some invalid paths to make sure they get properly
+ * rejected by the engine and not passed onto the user...
+ */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('a', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('a/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/b//a/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/b//a', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('a',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('a/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/b//a/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/b//a',)");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Invalid gluing of segments: '/a' + 'b' != '/ab' */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['b'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['b', 'c'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Also: '/a' + '/b' != '/a/b' */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['/b'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['', '/b'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Invalid (non-relative) changes */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['/'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['/a'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['a', '/a'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['a', 'a//b'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ /* Now try some real cases */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/', [''], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/:1::tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/one/key', [''], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/one/key:1::tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/two/', ['keys', 'here'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/two/:2:keys,here:tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/some/path/', ['a', 'b/', 'c/d'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/some/path/:3:a,b/,c/d:tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/other/key',)");
+ g_assert_cmpstr (change_log->str, ==, "w:/other/key:1::;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/other/dir/',)");
+ g_assert_cmpstr (change_log->str, ==, "w:/other/dir/:1::;");
+ g_string_set_size (change_log, 0);
+
+ dconf_engine_unref (engine);
+}
+
+static gboolean it_is_good_to_be_done;
+
+static gpointer
+waiter_thread (gpointer user_data)
+{
+ DConfEngine *engine = user_data;
+
+ dconf_engine_sync (engine);
+
+ g_assert (g_atomic_int_get (&it_is_good_to_be_done));
+
+ return NULL;
+}
+
+static void
+test_sync (void)
+{
+ GThread *waiter_threads[5];
+ DConfChangeset *change;
+ DConfEngine *engine;
+ GError *error = NULL;
+ gboolean success;
+ gint i;
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Make sure a waiter thread returns straight away if nothing is
+ * outstanding.
+ */
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ g_thread_join (g_thread_new ("waiter", waiter_thread, engine));
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ /* The write will try to check the system-db for a lock. That will
+ * fail because it doesn't exist...
+ */
+ change = dconf_changeset_new_write ("/value", g_variant_new_boolean (TRUE));
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "unable to open file");
+
+ /* Spin up some waiters */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ waiter_threads[i] = g_thread_new ("test waiter", waiter_thread, engine);
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ /* Release them by completing the pending async call */
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag"), NULL);
+ /* Make sure they all quit by joining them */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ g_thread_join (waiter_threads[i]);
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ /* Do the same again, but with a failure as a result */
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ waiter_threads[i] = g_thread_new ("test waiter", waiter_thread, engine);
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "some error");
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ dconf_mock_dbus_async_reply (NULL, error);
+ g_clear_error (&error);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "failed to commit changes to dconf: some error");
+
+ /* Make sure they all quit by joining them */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ g_thread_join (waiter_threads[i]);
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ /* Now put two changes in the queue and make sure we have to reply to
+ * both of them before the waiters finish.
+ */
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ waiter_threads[i] = g_thread_new ("test waiter", waiter_thread, engine);
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag1"), NULL);
+ /* Still should not have quit yet... wait a bit to let the waiters try
+ * to shoot themselves in their collective feet...
+ */
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ /* Will be OK after the second reply */
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag2"), NULL);
+ /* Make sure they all quit by joining them */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ g_thread_join (waiter_threads[i]);
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ dconf_changeset_unref (change);
+ dconf_engine_unref (engine);
+ dconf_mock_shm_reset ();
+}
+
+/* Log handling. */
+typedef struct
+{
+ GLogLevelFlags log_level;
+ GLogField *fields;
+ gsize n_fields;
+} LogMessage;
+
+static void
+log_message_clear (LogMessage *message)
+{
+ gsize i;
+
+ for (i = 0; i < message->n_fields; i++)
+ {
+ g_free ((gpointer) message->fields[i].key);
+ g_free ((gpointer) message->fields[i].value);
+ }
+}
+
+static GArray *logged_messages = NULL;
+
+static GLogWriterOutput
+log_writer_cb (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ LogMessage *message;
+ gsize i;
+
+ /* If we’re running as a subprocess, the parent process is going to be
+ * checking our stderr, so just behave normally. */
+ if (g_test_subprocess ())
+ return g_log_writer_default (log_level, fields, n_fields, user_data);
+
+ /* We only care about dconf messages and non-debug messages. */
+ if (log_level == G_LOG_LEVEL_DEBUG)
+ return G_LOG_WRITER_HANDLED;
+
+ for (i = 0; i < n_fields; i++)
+ {
+ if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0 &&
+ g_strcmp0 (fields[i].value, "dconf") != 0)
+ return G_LOG_WRITER_HANDLED;
+ }
+
+ /* Append the message to the queue. */
+ g_array_set_size (logged_messages, logged_messages->len + 1);
+ message = &g_array_index (logged_messages, LogMessage, logged_messages->len - 1);
+
+ message->log_level = log_level;
+ message->fields = g_new0 (GLogField, n_fields);
+ message->n_fields = n_fields;
+
+ for (i = 0; i < n_fields; i++)
+ {
+ gsize length = (fields[i].length < 0) ? strlen (fields[i].value) + 1 : fields[i].length;
+ message->fields[i].key = g_strdup (fields[i].key);
+ message->fields[i].value = g_malloc0 (length);
+ memcpy ((gpointer) message->fields[i].value, fields[i].value, length);
+ message->fields[i].length = fields[i].length;
+ }
+
+ return G_LOG_WRITER_HANDLED;
+}
+
+/* Assert there are no logged messages in the queue. */
+static void
+assert_no_messages (void)
+{
+ g_assert_cmpuint (logged_messages->len, ==, 0);
+}
+
+/* Assert there is at least one logged message in the queue, and the oldest
+ * logged message matches the given expectations. If so, pop it from the queue;
+ * if not, abort. */
+static void
+assert_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment)
+{
+ const LogMessage *logged_message;
+ gsize i;
+ const gchar *message = NULL, *domain = NULL;
+
+ g_assert_cmpuint (logged_messages->len, >, 0);
+ logged_message = &g_array_index (logged_messages, LogMessage, 0);
+
+ for (i = 0; i < logged_message->n_fields; i++)
+ {
+ if (g_strcmp0 (logged_message->fields[i].key, "MESSAGE") == 0)
+ message = logged_message->fields[i].value;
+ else if (g_strcmp0 (logged_message->fields[i].key, "GLIB_DOMAIN") == 0)
+ domain = logged_message->fields[i].value;
+ }
+
+ g_assert_cmpstr (domain, ==, expected_domain);
+ g_assert_cmpuint (logged_message->log_level, ==, expected_log_level);
+ g_assert_cmpstr (strstr (message, expected_message_fragment), !=, NULL);
+
+ g_array_remove_index (logged_messages, 0);
+}
+
+/* If there is at least one logged message in the queue, act like
+ * assert_pop_message(). Otherwise, if the queue is empty, return. */
+static void
+assert_maybe_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment)
+{
+ if (logged_messages->len == 0)
+ return;
+
+ assert_pop_message (expected_domain, expected_log_level, expected_message_fragment);
+}
+
+int
+main (int argc, char **argv)
+{
+ const ProfileParserOpenData profile_parser0 =
+ { SRCDIR "/profile/this-file-does-not-exist", "*WARNING*: unable to open named profile*" };
+ const ProfileParserOpenData profile_parser1 =
+ { SRCDIR "/profile/broken-profile", "*WARNING*: unknown dconf database*unknown dconf database*" };
+ const ProfileParserOpenData profile_parser2 =
+ { SRCDIR "/profile/gdm", "*WARNING*: unknown dconf database*unknown dconf database*" };
+ int retval;
+
+ g_setenv ("XDG_RUNTIME_DIR", "/RUNTIME/", TRUE);
+ g_setenv ("XDG_CONFIG_HOME", "/HOME/.config", TRUE);
+ g_unsetenv ("DCONF_PROFILE");
+
+ logged_messages = g_array_new (FALSE, FALSE, sizeof (LogMessage));
+ g_array_set_clear_func (logged_messages, (GDestroyNotify) log_message_clear);
+ g_log_set_writer_func (log_writer_cb, NULL, NULL);
+
+ main_thread = g_thread_self ();
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_data_func ("/engine/profile-parser/errors/0", &profile_parser0, test_profile_parser_errors);
+ g_test_add_data_func ("/engine/profile-parser/errors/1", &profile_parser1, test_profile_parser_errors);
+ g_test_add_data_func ("/engine/profile-parser/errors/2", &profile_parser2, test_profile_parser_errors);
+ g_test_add_func ("/engine/profile-parser", test_profile_parser);
+ g_test_add_func ("/engine/signal-threadsafety", test_signal_threadsafety);
+ g_test_add_func ("/engine/sources/user", test_user_source);
+ g_test_add_func ("/engine/sources/system", test_system_source);
+ g_test_add_func ("/engine/sources/file", test_file_source);
+ g_test_add_func ("/engine/sources/service", test_service_source);
+ g_test_add_func ("/engine/read", test_read);
+ g_test_add_func ("/engine/watch/fast", test_watch_fast);
+ g_test_add_func ("/engine/watch/fast/simultaneous", test_watch_fast_simultaneous_subscriptions);
+ g_test_add_func ("/engine/watch/fast/successive", test_watch_fast_successive_subscriptions);
+ g_test_add_func ("/engine/watch/fast/short_lived", test_watch_fast_short_lived_subscriptions);
+ g_test_add_func ("/engine/watch/sync", test_watch_sync);
+ g_test_add_func ("/engine/change/fast", test_change_fast);
+ g_test_add_func ("/engine/change/sync", test_change_sync);
+ g_test_add_func ("/engine/signals", test_signals);
+ g_test_add_func ("/engine/sync", test_sync);
+
+ retval = g_test_run ();
+
+ assert_no_messages ();
+ g_array_unref (logged_messages);
+
+ return retval;
+}
diff --git a/tests/gvdb.c b/tests/gvdb.c
new file mode 100644
index 0000000..d054067
--- /dev/null
+++ b/tests/gvdb.c
@@ -0,0 +1,438 @@
+#include <glib.h>
+#include "../gvdb/gvdb-reader.h"
+
+static void
+test_reader_open_error (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/does_not_exist", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/file_empty", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/invalid_header", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/file_too_small", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+}
+
+static void
+test_reader_empty (void)
+{
+ const gchar * strings[] = { "", "value", "/value", ".", NULL};
+ GError *error = NULL;
+ GvdbTable *table;
+ gchar **names;
+ gint n_names;
+ gint i;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/empty_gvdb", TRUE, &error);
+ g_assert_no_error (error);
+ g_assert (table != NULL);
+
+ g_assert (gvdb_table_is_valid (table));
+
+ names = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, 0);
+ g_assert_cmpint (g_strv_length (names), ==, 0);
+ g_strfreev (names);
+
+ names = gvdb_table_get_names (table, NULL);
+ g_assert_cmpint (g_strv_length (names), ==, 0);
+ g_strfreev (names);
+
+ for (i = 0; strings[i]; i++)
+ {
+ const gchar *key = strings[i];
+ GvdbTable *sub;
+ GVariant *val;
+ gboolean has;
+ gchar **list;
+
+ sub = gvdb_table_get_table (table, key);
+ g_assert (sub == NULL);
+
+ has = gvdb_table_has_value (table, key);
+ g_assert (!has);
+
+ val = gvdb_table_get_value (table, key);
+ g_assert (val == NULL);
+
+ val = gvdb_table_get_raw_value (table, key);
+ g_assert (val == NULL);
+
+ list = gvdb_table_list (table, key);
+ g_assert (list == NULL);
+ }
+
+ gvdb_table_free (table);
+}
+
+static void
+verify_table (GvdbTable *table)
+{
+ GVariant *value;
+ gchar **list;
+ gint n_names;
+ gboolean has;
+
+ /* We could not normally expect these to be in a particular order but
+ * we are using a specific test file that we know to be layed out this
+ * way...
+ *
+ * It's pure luck that they happened to be layed out in this nice way.
+ */
+ list = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, g_strv_length (list));
+ g_assert_cmpint (n_names, ==, 5);
+ g_assert_cmpstr (list[0], ==, "/");
+ g_assert_cmpstr (list[1], ==, "/values/");
+ g_assert_cmpstr (list[2], ==, "/values/boolean");
+ g_assert_cmpstr (list[3], ==, "/values/string");
+ g_assert_cmpstr (list[4], ==, "/values/int32");
+ g_strfreev (list);
+
+ list = gvdb_table_list (table, "/");
+ g_assert (list != NULL);
+ g_assert_cmpint (g_strv_length (list), ==, 1);
+ g_assert_cmpstr (list[0], ==, "values/");
+ g_strfreev (list);
+
+ list = gvdb_table_list (table, "/values/");
+ g_assert (list != NULL);
+ g_assert_cmpint (g_strv_length (list), ==, 3);
+ g_assert_cmpstr (list[0], ==, "boolean");
+ g_assert_cmpstr (list[1], ==, "int32");
+ g_assert_cmpstr (list[2], ==, "string");
+ g_strfreev (list);
+
+ /* A directory is not a value */
+ has = gvdb_table_has_value (table, "/");
+ g_assert (!has);
+ has = gvdb_table_has_value (table, "/values/");
+ g_assert (!has);
+
+ has = gvdb_table_has_value (table, "/int32");
+ g_assert (!has);
+ has = gvdb_table_has_value (table, "values/int32");
+ g_assert (!has);
+ has = gvdb_table_has_value (table, "/values/int32");
+ g_assert (has);
+
+ value = gvdb_table_get_value (table, "/");
+ g_assert (value == NULL);
+ value = gvdb_table_get_value (table, "/values/");
+ g_assert (value == NULL);
+ value = gvdb_table_get_value (table, "/int32");
+ g_assert (value == NULL);
+ value = gvdb_table_get_value (table, "values/int32");
+ g_assert (value == NULL);
+
+ value = gvdb_table_get_value (table, "/values/boolean");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN));
+ g_assert (g_variant_get_boolean (value));
+ g_variant_unref (value);
+
+ value = gvdb_table_get_raw_value (table, "/values/boolean");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN));
+ g_assert (g_variant_get_boolean (value));
+ g_variant_unref (value);
+
+ value = gvdb_table_get_value (table, "/values/int32");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_INT32));
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 0x44332211);
+ g_variant_unref (value);
+
+ value = gvdb_table_get_value (table, "/values/string");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_STRING));
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "a string");
+ g_variant_unref (value);
+
+ value = gvdb_table_get_raw_value (table, "/values/string");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_STRING));
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "a string");
+ g_variant_unref (value);
+}
+
+static void
+test_reader_values (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/example_gvdb", TRUE, &error);
+ g_assert_no_error (error);
+ verify_table (table);
+
+#if G_BYTE_ORDER == G_BIG_ENDIAN
+ {
+ GVariant *value;
+
+ value = gvdb_table_get_raw_value (table, "/values/int32");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_INT32));
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 0x11223344);
+ g_variant_unref (value);
+ }
+#endif
+
+ gvdb_table_free (table);
+}
+
+static void
+test_reader_values_bigendian (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/example_gvdb.big-endian", TRUE, &error);
+ g_assert_no_error (error);
+ verify_table (table);
+
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+ {
+ GVariant *value;
+
+ value = gvdb_table_get_raw_value (table, "/values/int32");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_INT32));
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 0x11223344);
+ g_variant_unref (value);
+ }
+#endif
+
+ gvdb_table_free (table);
+}
+
+static void
+test_nested (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+ GvdbTable *locks;
+ gchar **names;
+ gint n_names;
+ gboolean has;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/nested_gvdb", TRUE, &error);
+ g_assert_no_error (error);
+
+ /* Note the more-random ordering here compared with above. */
+ names = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, g_strv_length (names));
+ g_assert_cmpint (n_names, ==, 6);
+ g_assert_cmpstr (names[0], ==, "/values/boolean");
+ g_assert_cmpstr (names[1], ==, "/");
+ g_assert_cmpstr (names[2], ==, "/values/int32");
+ g_assert_cmpstr (names[3], ==, ".locks");
+ g_assert_cmpstr (names[4], ==, "/values/");
+ g_assert_cmpstr (names[5], ==, "/values/string");
+ g_strfreev (names);
+
+ locks = gvdb_table_get_table (table, "/");
+ g_assert (locks == NULL);
+ locks = gvdb_table_get_table (table, "/values/");
+ g_assert (locks == NULL);
+ locks = gvdb_table_get_table (table, "/values/int32");
+ g_assert (locks == NULL);
+
+ locks = gvdb_table_get_table (table, ".locks");
+ g_assert (locks != NULL);
+
+ has = gvdb_table_has_value (locks, "/first/lck");
+ g_assert (!has);
+
+ has = gvdb_table_has_value (locks, "/first/lock");
+ g_assert (has);
+
+ has = gvdb_table_has_value (locks, "/second");
+ g_assert (has);
+
+ gvdb_table_free (table);
+ gvdb_table_free (locks);
+}
+
+/* This function exercises the API against @table but does not do any
+ * asserts on unexpected values (although it will assert on inconsistent
+ * values returned by the API).
+ */
+static void
+inspect_carefully (GvdbTable *table,
+ gint level)
+{
+ const gchar * key_names[] = {
+ "/", "/values/", "/int32", "values/int32",
+ "/values/int32", "/values/boolean", "/values/string",
+ ".locks", "/first/lock", "/second", NULL
+ };
+ gint found_items;
+ gchar **names;
+ gint n_names;
+ gint i;
+
+ if (level > 100)
+ return;
+
+ found_items = 0;
+ for (i = 0; key_names[i]; i++)
+ {
+ const gchar *key = key_names[i];
+ GvdbTable *subtable;
+ GVariant *value;
+ gchar **list;
+ gboolean has;
+
+ has = gvdb_table_has_value (table, key);
+
+ list = gvdb_table_list (table, key);
+ g_assert (!has || list == NULL);
+ if (list)
+ {
+ gchar *joined = g_strjoinv (",", list);
+ g_strfreev (list);
+ g_free (joined);
+ found_items++;
+ }
+
+ value = gvdb_table_get_value (table, key);
+ g_assert_cmpint (value != NULL, ==, has);
+ if (value)
+ {
+ gchar *printed = g_variant_print (value, FALSE);
+ g_variant_unref (value);
+ g_free (printed);
+ found_items++;
+ }
+
+ value = gvdb_table_get_raw_value (table, key);
+ g_assert_cmpint (value != NULL, ==, has);
+ if (value)
+ {
+ gchar *printed = g_variant_print (value, FALSE);
+ g_variant_unref (value);
+ g_free (printed);
+ }
+
+ subtable = gvdb_table_get_table (table, key);
+ g_assert (!has || subtable == NULL);
+ if (subtable)
+ {
+ inspect_carefully (subtable, level + 1);
+ gvdb_table_free (subtable);
+ found_items++;
+ }
+ }
+
+ names = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, g_strv_length (names));
+ g_assert_cmpint (found_items, <=, n_names);
+ g_free (g_strjoinv (" ", names));
+ g_strfreev (names);
+}
+
+static void
+test_corrupted (gconstpointer user_data)
+{
+ gint percentage = GPOINTER_TO_INT (user_data);
+ GError *error = NULL;
+ GMappedFile *mapped;
+
+ mapped = g_mapped_file_new (SRCDIR "/gvdbs/nested_gvdb", FALSE, &error);
+ g_assert_no_error (error);
+ g_assert (mapped);
+
+ if (percentage)
+ {
+ GvdbTable *table;
+ const gchar *orig;
+ gsize length;
+ gchar *copy;
+ gint i;
+
+ orig = g_mapped_file_get_contents (mapped);
+ length = g_mapped_file_get_length (mapped);
+ copy = g_memdup (orig, length);
+
+ for (i = 0; i < 10000; i++)
+ {
+ GBytes *bytes;
+ gint j;
+
+ /* Make a broken copy, but leave the signature intact so that
+ * we don't get too many boring trivial failures.
+ */
+ for (j = 8; j < length; j++)
+ if (g_test_rand_int_range (0, 100) < percentage)
+ copy[j] = g_test_rand_int_range (0, 256);
+ else
+ copy[j] = orig[j];
+
+ bytes = g_bytes_new_static (copy, length);
+ table = gvdb_table_new_from_bytes (bytes, FALSE, &error);
+ g_bytes_unref (bytes);
+
+ /* If we damaged the header, it may not open */
+ if (table)
+ {
+ inspect_carefully (table, 0);
+ gvdb_table_free (table);
+ }
+ else
+ {
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_clear_error (&error);
+ }
+ }
+
+ g_free (copy);
+ }
+ else
+ {
+ GvdbTable *table;
+ GBytes *bytes;
+
+ bytes = g_mapped_file_get_bytes (mapped);
+ table = gvdb_table_new_from_bytes (bytes, FALSE, &error);
+ g_bytes_unref (bytes);
+
+ g_assert_no_error (error);
+ g_assert (table);
+
+ inspect_carefully (table, 0);
+ gvdb_table_free (table);
+ }
+
+ g_mapped_file_unref (mapped);
+}
+
+int
+main (int argc, char **argv)
+{
+ gint i;
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/gvdb/reader/open-error", test_reader_open_error);
+ g_test_add_func ("/gvdb/reader/empty", test_reader_empty);
+ g_test_add_func ("/gvdb/reader/values", test_reader_values);
+ g_test_add_func ("/gvdb/reader/values/big-endian", test_reader_values_bigendian);
+ g_test_add_func ("/gvdb/reader/nested", test_nested);
+ for (i = 0; i < 20; i++)
+ {
+ gchar test_name[80];
+ g_snprintf (test_name, sizeof test_name, "/gvdb/reader/corrupted/%d%%", i);
+ g_test_add_data_func (test_name, GINT_TO_POINTER (i), test_corrupted);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/gvdbs/empty_gvdb b/tests/gvdbs/empty_gvdb
new file mode 100644
index 0000000..c700bdb
--- /dev/null
+++ b/tests/gvdbs/empty_gvdb
Binary files differ
diff --git a/tests/gvdbs/example_gvdb b/tests/gvdbs/example_gvdb
new file mode 100644
index 0000000..73b098e
--- /dev/null
+++ b/tests/gvdbs/example_gvdb
Binary files differ
diff --git a/tests/gvdbs/example_gvdb.big-endian b/tests/gvdbs/example_gvdb.big-endian
new file mode 100644
index 0000000..c729546
--- /dev/null
+++ b/tests/gvdbs/example_gvdb.big-endian
Binary files differ
diff --git a/tests/gvdbs/file_empty b/tests/gvdbs/file_empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/gvdbs/file_empty
diff --git a/tests/gvdbs/file_too_small b/tests/gvdbs/file_too_small
new file mode 100644
index 0000000..b54857d
--- /dev/null
+++ b/tests/gvdbs/file_too_small
@@ -0,0 +1 @@
+GVariant
diff --git a/tests/gvdbs/invalid_header b/tests/gvdbs/invalid_header
new file mode 100644
index 0000000..25335ec
--- /dev/null
+++ b/tests/gvdbs/invalid_header
@@ -0,0 +1 @@
+GVARIANT________________ \ No newline at end of file
diff --git a/tests/gvdbs/nested_gvdb b/tests/gvdbs/nested_gvdb
new file mode 100644
index 0000000..b593e38
--- /dev/null
+++ b/tests/gvdbs/nested_gvdb
Binary files differ
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 0000000..8aa5837
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,61 @@
+sources = files(
+ 'dconf-mock-dbus.c',
+ 'dconf-mock-gvdb.c',
+ 'dconf-mock-shm.c',
+)
+
+libdconf_mock = static_library(
+ 'dconf-mock',
+ sources: sources,
+ dependencies: glib_dep,
+)
+
+envs = test_env + [
+ 'G_TEST_SRCDIR=' + meson.current_source_dir(),
+ 'G_TEST_BUILDDIR=' + meson.current_build_dir(),
+]
+
+test_dir = meson.current_source_dir()
+
+dl_dep = cc.find_library('dl', required: false)
+m_dep = cc.find_library('m')
+
+unit_tests = [
+ # [name, sources, c_args, dependencies, link_with]
+ ['paths', 'paths.c', [], libdconf_common_dep, []],
+ ['changeset', 'changeset.c', [], libdconf_common_dep, []],
+ ['shm', ['shm.c', 'tmpdir.c'], [], [dl_dep, libdconf_common_dep, libdconf_shm_test_dep], []],
+ ['gvdb', 'gvdb.c', '-DSRCDIR="@0@"'.format(test_dir), libgvdb_dep, []],
+ ['gdbus-thread', 'dbus.c', '-DDBUS_BACKEND="/gdbus/thread"', libdconf_gdbus_thread_dep, []],
+ ['gdbus-filter', 'dbus.c', '-DDBUS_BACKEND="/gdbus/filter"', libdconf_gdbus_filter_dep, []],
+ ['engine', 'engine.c', '-DSRCDIR="@0@"'.format(test_dir), [dl_dep, libdconf_engine_test_dep, m_dep], libdconf_mock],
+ ['client', 'client.c', '-DSRCDIR="@0@"'.format(test_dir), [libdconf_client_dep, libdconf_engine_dep], libdconf_mock],
+ ['writer', 'writer.c', '-DSRCDIR="@0@"'.format(test_dir), [glib_dep, dl_dep, m_dep, libdconf_service_dep], [libdconf_mock]],
+]
+
+foreach unit_test: unit_tests
+ exe = executable(
+ unit_test[0],
+ unit_test[1],
+ c_args: unit_test[2],
+ dependencies: unit_test[3],
+ link_with: unit_test[4],
+ include_directories: [top_inc, include_directories('../service')],
+ )
+
+ test(unit_test[0], exe, is_parallel: false, env: envs)
+endforeach
+
+python3 = find_program('python3', required: false)
+dbus_daemon = find_program('dbus-daemon', required: false)
+
+if python3.found() and dbus_daemon.found()
+ test_dconf_py = find_program('test-dconf.py')
+ test(
+ 'dconf',
+ test_dconf_py,
+ args: [dconf.full_path(), dconf_service.full_path()]
+ )
+else
+ message('Skipping dconf test because python3 or dbus-daemon is not available')
+endif
diff --git a/tests/paths.c b/tests/paths.c
new file mode 100644
index 0000000..5774333
--- /dev/null
+++ b/tests/paths.c
@@ -0,0 +1,98 @@
+#include "../common/dconf-paths.h"
+
+static void
+test_paths (void)
+{
+ struct test_case {
+ const gchar *string;
+ guint flags;
+ } cases[] = {
+
+#define invalid 0
+#define path 001
+#define key 002 | path
+#define dir 004 | path
+#define rel 010
+#define relkey 020 | rel
+#define reldir 040 | rel
+
+ { NULL, invalid },
+ { "", reldir },
+ { "/", dir },
+
+ { "/key", key },
+ { "/path/", dir },
+ { "/path/key", key },
+ { "/path/path/", dir },
+ { "/a/b", key },
+ { "/a/b/", dir },
+
+ { "//key", invalid },
+ { "//path/", invalid },
+ { "//path/key", invalid },
+ { "//path/path/", invalid },
+ { "//a/b", invalid },
+ { "//a/b/", invalid },
+
+ { "/key", key },
+ { "/path//", invalid },
+ { "/path/key", key },
+ { "/path/path//", invalid },
+ { "/a/b", key },
+ { "/a/b//", invalid },
+
+ { "/key", key },
+ { "/path/", dir },
+ { "/path//key", invalid },
+ { "/path//path/", invalid },
+ { "/a//b", invalid },
+ { "/a//b/", invalid },
+
+ { "key", relkey },
+ { "path/", reldir },
+ { "path/key", relkey },
+ { "path/path/", reldir },
+ { "a/b", relkey },
+ { "a/b/", reldir },
+
+ { "key", relkey },
+ { "path//", invalid },
+ { "path/key", relkey },
+ { "path/path//", invalid },
+ { "a/b", relkey },
+ { "a/b//", invalid },
+
+ { "key", relkey },
+ { "path/", reldir },
+ { "path//key", invalid },
+ { "path//path/", invalid },
+ { "a//b", invalid },
+ { "a//b/", invalid }
+ };
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (cases); i++)
+ {
+ const gchar *string = cases[i].string;
+ guint flags;
+
+ flags = (dconf_is_path (string, NULL) ? 001 : 000) |
+ (dconf_is_key (string, NULL) ? 002 : 000) |
+ (dconf_is_dir (string, NULL) ? 004 : 000) |
+ (dconf_is_rel_path (string, NULL) ? 010 : 000) |
+ (dconf_is_rel_key (string, NULL) ? 020 : 000) |
+ (dconf_is_rel_dir (string, NULL) ? 040 : 000);
+
+ g_assert_cmphex (flags, ==, cases[i].flags);
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/paths", test_paths);
+
+ return g_test_run ();
+}
diff --git a/tests/profile/broken-profile b/tests/profile/broken-profile
new file mode 100644
index 0000000..6bdcb24
--- /dev/null
+++ b/tests/profile/broken-profile
@@ -0,0 +1,6 @@
+a b c d e
+f g h i j
+bad-db:foo
+user-db:
+user-dd:xyz
+system-dd:wer
diff --git a/tests/profile/colourful b/tests/profile/colourful
new file mode 100644
index 0000000..5a69bb5
--- /dev/null
+++ b/tests/profile/colourful
@@ -0,0 +1,13 @@
+# this is an interesting dconf profile file
+ # it shows some of the possible weird things you can do
+
+ user-db:user # comments can be like this
+
+# we can have comments that are extremely long. we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.
+
+ system-db:other # we can have comments that are extremely long. we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.
+
+system-db:verylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongname
+
+# there is no newline after this last line
+system-db:nonewline \ No newline at end of file
diff --git a/tests/profile/dos b/tests/profile/dos
new file mode 100644
index 0000000..cc38458
--- /dev/null
+++ b/tests/profile/dos
@@ -0,0 +1,2 @@
+user-db:user
+system-db:site
diff --git a/tests/profile/empty-profile b/tests/profile/empty-profile
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/profile/empty-profile
diff --git a/tests/profile/gdm b/tests/profile/gdm
new file mode 100644
index 0000000..d5a90e5
--- /dev/null
+++ b/tests/profile/gdm
@@ -0,0 +1,2 @@
+user
+gdm
diff --git a/tests/profile/many-sources b/tests/profile/many-sources
new file mode 100644
index 0000000..15fe5ca
--- /dev/null
+++ b/tests/profile/many-sources
@@ -0,0 +1,10 @@
+user-db:user
+system-db:local
+system-db:room
+system-db:floor
+system-db:building
+system-db:site
+system-db:region
+system-db:division
+system-db:country
+system-db:global
diff --git a/tests/profile/no-newline-longline b/tests/profile/no-newline-longline
new file mode 100644
index 0000000..93193aa
--- /dev/null
+++ b/tests/profile/no-newline-longline
@@ -0,0 +1 @@
+# this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. \ No newline at end of file
diff --git a/tests/profile/test-profile b/tests/profile/test-profile
new file mode 100644
index 0000000..3a511c2
--- /dev/null
+++ b/tests/profile/test-profile
@@ -0,0 +1 @@
+user-db:test
diff --git a/tests/profile/will-never-exist b/tests/profile/will-never-exist
new file mode 100644
index 0000000..bcff7a6
--- /dev/null
+++ b/tests/profile/will-never-exist
@@ -0,0 +1 @@
+user-db:will-never-exist
diff --git a/tests/shm.c b/tests/shm.c
new file mode 100644
index 0000000..69d683f
--- /dev/null
+++ b/tests/shm.c
@@ -0,0 +1,175 @@
+#define _GNU_SOURCE
+
+#include "../common/dconf-paths.h"
+#include <glib/gstdio.h>
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "../shm/dconf-shm.h"
+#include "../shm/dconf-shm-mockable.h"
+#include "tmpdir.h"
+
+static void
+test_mkdir_fail (void)
+{
+ guint8 *shm;
+
+ if (g_test_subprocess ())
+ {
+ gchar *evil;
+ gint fd;
+
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ evil = g_build_filename (g_get_user_runtime_dir (), "dconf", NULL);
+ fd = open (evil, O_WRONLY | O_CREAT, 0600);
+ close (fd);
+
+ shm = dconf_shm_open ("foo");
+ g_assert (shm == NULL);
+
+ g_unlink (evil);
+ g_free (evil);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*unable to create directory*");
+}
+
+static void
+test_close_null (void)
+{
+ dconf_shm_close (NULL);
+}
+
+static void
+test_open_and_flag (void)
+{
+ guint8 *shm;
+
+ shm = dconf_shm_open ("foo");
+ g_assert (shm != NULL);
+ g_assert (!dconf_shm_is_flagged (shm));
+ dconf_shm_flag ("foo");
+ g_assert (dconf_shm_is_flagged (shm));
+ dconf_shm_close (shm);
+}
+
+static void
+test_invalid_name (void)
+{
+ if (g_test_subprocess ())
+ {
+ guint8 *shm;
+
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ shm = dconf_shm_open ("foo/bar");
+ g_assert (shm == NULL);
+ g_assert (dconf_shm_is_flagged (shm));
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*unable to create*foo/bar*");
+}
+
+static void
+test_flag_nonexistent (void)
+{
+ dconf_shm_flag ("does-not-exist");
+}
+
+static gboolean should_fail_pwrite;
+/* interpose */
+ssize_t
+dconf_shm_pwrite (int fd, const void *buf, size_t count, off_t offset)
+{
+ if (should_fail_pwrite)
+ {
+ errno = ENOSPC;
+ return -1;
+ }
+
+ return pwrite (fd, buf, count, offset);
+}
+
+static void
+test_out_of_space_open (void)
+{
+ if (g_test_subprocess ())
+ {
+ guint8 *shm;
+
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+ should_fail_pwrite = TRUE;
+
+ shm = dconf_shm_open ("foo");
+ g_assert (shm == NULL);
+ g_assert (dconf_shm_is_flagged (shm));
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*failed to allocate*foo*");
+}
+
+static void
+test_out_of_space_flag (void)
+{
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+ should_fail_pwrite = TRUE;
+
+ dconf_shm_flag ("foo");
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+int
+main (int argc, char **argv)
+{
+ gchar *temp;
+ gint status;
+
+ temp = dconf_test_create_tmpdir ();
+
+ g_setenv ("XDG_RUNTIME_DIR", temp, TRUE);
+ /* This currently works, but it is possible that one day GLib will
+ * read the XDG_RUNTIME_DIR variable (and cache its value) as a
+ * side-effect of the dconf_test_create_tmpdir() call above.
+ *
+ * This assert will quickly uncover the problem in that case...
+ */
+ g_assert_cmpstr (g_get_user_runtime_dir (), ==, temp);
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/shm/mkdir-fail", test_mkdir_fail);
+ g_test_add_func ("/shm/close-null", test_close_null);
+ g_test_add_func ("/shm/open-and-flag", test_open_and_flag);
+ g_test_add_func ("/shm/invalid-name", test_invalid_name);
+ g_test_add_func ("/shm/flag-nonexistent", test_flag_nonexistent);
+ g_test_add_func ("/shm/out-of-space-open", test_out_of_space_open);
+ g_test_add_func ("/shm/out-of-space-flag", test_out_of_space_flag);
+
+ status = g_test_run ();
+
+ dconf_test_remove_tmpdir (temp);
+ g_free (temp);
+
+ return status;
+}
diff --git a/tests/test-dconf.py b/tests/test-dconf.py
new file mode 100755
index 0000000..6cd80a8
--- /dev/null
+++ b/tests/test-dconf.py
@@ -0,0 +1,817 @@
+#!/usr/bin/env python3
+#
+# Copyright © 2018 Tomasz Miąsko
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the licence, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, see <http://www.gnu.org/licenses/>.
+
+import mmap
+import os
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+
+from textwrap import dedent
+
+DAEMON_CONFIG = '''
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+ <type>session</type>
+ <keep_umask/>
+ <listen>unix:tmpdir=/tmp</listen>
+ <servicedir>{servicedir}</servicedir>
+ <auth>EXTERNAL</auth>
+ <policy context="default">
+ <allow send_destination="*" eavesdrop="true"/>
+ <allow eavesdrop="true"/>
+ <allow own="*"/>
+ </policy>
+</busconfig>
+'''
+
+SERVICE_CONFIG = '''\
+[D-BUS Service]
+Name={name}
+Exec={exec}
+'''
+
+
+def dconf(*args, **kwargs):
+ argv = [dconf_exe]
+ argv.extend(args)
+
+ # Setup convenient defaults:
+ kwargs.setdefault('check', True)
+ kwargs.setdefault('stdout', subprocess.PIPE)
+ kwargs.setdefault('universal_newlines', True)
+
+ return subprocess.run(argv, **kwargs)
+
+
+def dconf_read(key, **kwargs):
+ return dconf('read', key, **kwargs).stdout.rstrip('\n')
+
+
+def dconf_write(key, value):
+ dconf('write', key, value)
+
+
+def dconf_list(key):
+ return dconf('list', key).stdout.splitlines()
+
+def dconf_locks(key, **kwargs):
+ lines = dconf('list-locks', key, **kwargs).stdout.splitlines()
+ lines.sort()
+ return lines
+
+def dconf_complete(suffix, prefix):
+ lines = dconf('_complete', suffix, prefix).stdout.splitlines()
+ lines.sort()
+ return lines
+
+
+def dconf_watch(path):
+ args = [dconf_exe, 'watch', path]
+ return subprocess.Popen(args,
+ stdout=subprocess.PIPE,
+ universal_newlines=True)
+
+
+class DBusTest(unittest.TestCase):
+
+ def setUp(self):
+ self.temporary_dir = tempfile.TemporaryDirectory()
+
+ self.runtime_dir = os.path.join(self.temporary_dir.name, 'run')
+ self.config_home = os.path.join(self.temporary_dir.name, 'config')
+ self.dbus_dir = os.path.join(self.temporary_dir.name, 'dbus-1')
+
+ os.mkdir(self.runtime_dir, mode=0o700)
+ os.mkdir(self.config_home, mode=0o700)
+ os.mkdir(self.dbus_dir, mode=0o700)
+ os.mkdir(os.path.join(self.config_home, 'dconf'))
+
+ os.environ['DCONF_BLAME'] = ''
+ os.environ['XDG_RUNTIME_DIR'] = self.runtime_dir
+ os.environ['XDG_CONFIG_HOME'] = self.config_home
+
+ # Prepare dbus-daemon config.
+ dbus_daemon_config = os.path.join(self.dbus_dir, 'session.conf')
+ with open(dbus_daemon_config, 'w') as file:
+ file.write(DAEMON_CONFIG.format(servicedir=self.dbus_dir))
+
+ # Prepare service config.
+ name = 'ca.desrt.dconf'
+ path = os.path.join(self.dbus_dir, '{}.service'.format(name))
+ with open(path, 'w') as file:
+ config = SERVICE_CONFIG.format(name=name, exec=dconf_service_exe)
+ file.write(config)
+
+ # Pipe where daemon will write its address.
+ read_fd, write_fd = os.pipe2(0)
+
+ args = ['dbus-daemon',
+ '--config-file={}'.format(dbus_daemon_config),
+ '--nofork',
+ '--print-address={}'.format(write_fd)]
+
+ # Start daemon
+ self.dbus_daemon_process = subprocess.Popen(args, pass_fds=[write_fd])
+
+ # Close our writing end of pipe. Daemon closes its own writing end of
+ # pipe after printing address, so subsequent reads shouldn't block.
+ os.close(write_fd)
+
+ with os.fdopen(read_fd) as f:
+ dbus_address = f.read().rstrip()
+
+ # Prepare environment
+ os.environ['DBUS_SESSION_BUS_ADDRESS'] = dbus_address
+
+ def tearDown(self):
+ # Terminate dbus-daemon.
+ p = self.dbus_daemon_process
+ try:
+ p.terminate()
+ p.wait(timeout=0.5)
+ except subprocess.TimeoutExpired:
+ p.kill()
+ p.wait()
+
+ self.temporary_dir.cleanup()
+
+ def test_invalid_usage(self):
+ """Invalid dconf usage results in non-zero exit code and help message.
+ """
+ cases = [
+ # No command:
+ [],
+
+ # Invalid command:
+ ['no-such-command'],
+
+ # Too many arguments:
+ ['blame', 'a'],
+
+ # Missing arguments:
+ ['compile'],
+ ['compile', 'output'],
+ # Too many arguments:
+ ['compile', 'output', 'dir1', 'dir2'],
+
+ # Missing arguments:
+ ['_complete'],
+ ['_complete', ''],
+ # Too many arguments:
+ ['_complete', '', '/', '/'],
+
+ # Missing argument:
+ ['dump'],
+ # Dir is required:
+ ['dump', '/key'],
+ # Too many arguments:
+ ['dump', '/a/', '/b/'],
+
+ # Missing argument:
+ ['list'],
+ # Dir is required:
+ ['list', '/foo/bar'],
+ # Too many arguments:
+ ['list', '/foo', '/bar'],
+
+ # Missing argument:
+ ['list-locks'],
+ # Dir is required:
+ ['list-locks', '/key'],
+ # Too many arguments:
+ ['list-locks', '/a/', '/b/'],
+
+ # Missing argument:
+ ['load'],
+ # Dir is required:
+ ['load', '/key'],
+ # Too many arguments:
+ ['load', '/a/', '/b/'],
+
+ # Missing argument:
+ ['read'],
+ # Key is required:
+ ['read', '/dir/'],
+ # Too many arguments:
+ ['read', '/a', '/b'],
+ ['read', '-d', '/a', '/b'],
+
+ # Missing arguments:
+ ['reset'],
+ # Invalid path:
+ ['reset', 'test/test'],
+ # Too many arguments:
+ ['reset', '/test', '/test'],
+ ['reset', '-f', '/', '/'],
+
+ # Missing arguments:
+ ['watch'],
+ # Invalid path:
+ ['watch', 'foo'],
+ # Too many arguments:
+ ['watch', '/a', '/b'],
+
+ # Missing arguments:
+ ['write'],
+ ['write', '/key'],
+ # Invalid value:
+ ['write', '/key', 'not-a-gvariant-value'],
+ # Too many arguments:
+ ['write', '/key', '1', '2'],
+
+ # Too many arguments:
+ ['update', 'a', 'b'],
+ ]
+
+ for args in cases:
+ with self.subTest(args=args):
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf(*args, stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, 'Usage:')
+
+ def test_help(self):
+ """Help show usage information on stdout and exits with success."""
+
+ stdout = dconf('help', 'write').stdout
+ self.assertRegex(stdout, 'dconf write KEY VALUE')
+
+ stdout = dconf('help', 'help').stdout
+ self.assertRegex(stdout, 'dconf help COMMAND')
+
+ def test_read_nonexisiting(self):
+ """Reading missing key produces no output. """
+
+ self.assertEqual('', dconf_read('/key'))
+
+ def test_write_read(self):
+ """Read returns previously written value."""
+
+ dconf('write', '/key', '0')
+ self.assertEqual('0', dconf_read('/key'))
+
+ dconf('write', '/key', '"hello there"')
+ self.assertEqual("'hello there'", dconf_read('/key'))
+
+ def test_list(self):
+ """List returns a list of names inside given directory.
+
+ Results include both keys and subdirectories.
+ """
+
+ dconf('write', '/org/gnome/app/fullscreen', 'true')
+ dconf('write', '/org/gnome/terminal/profile', '"default"')
+ dconf('write', '/key', '42')
+
+ self.assertEqual(['key', 'org/'], dconf_list('/'))
+
+ self.assertEqual(['gnome/'], dconf_list('/org/'))
+
+ def test_list_missing(self):
+ """List can be used successfully with non existing directories. """
+
+ self.assertEqual([], dconf_list('/no-existing/directory/'))
+
+ def test_reset_key(self):
+ """Reset can be used to reset a value of a single key."""
+
+ dconf('write', '/app/width', '1024')
+ dconf('write', '/app/height', '768')
+ dconf('write', '/app/fullscreen', 'true')
+
+ # Sanity check.
+ self.assertEqual(['fullscreen', 'height', 'width'], dconf_list('/app/'))
+
+ # Reset one key after another:
+ dconf('reset', '/app/fullscreen')
+ self.assertEqual(['height', 'width'], dconf_list('/app/'))
+ dconf('reset', '/app/width')
+ self.assertEqual(['height'], dconf_list('/app/'))
+ dconf('reset', '/app/height')
+ self.assertEqual([], dconf_list('/app/'))
+
+ def test_reset_dir(self):
+ """Reseting whole directory is possible with -f option.
+
+ It is an error not to use -f when resetting a dir.
+ """
+
+ dconf('write', '/app/a', '1')
+ dconf('write', '/app/b', '2')
+ dconf('write', '/app/c/d', '3')
+ dconf('write', '/x', '4')
+ dconf('write', '/y/z', '5')
+
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('reset', '/app/', stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, '-f must be given')
+ self.assertRegex(cm.exception.stderr, 'Usage:')
+
+ # Nothing should be removed just yet.
+ self.assertTrue(['a', 'b', 'c'], dconf_list('/app/'))
+
+ # Try again with -f.
+ dconf('reset', '-f', '/app/')
+
+ # /app/ should be gone now:
+ self.assertEqual(['x', 'y/'], dconf_list('/'))
+
+ def test_watch(self):
+ """Watch reports changes made using write command.
+
+ Only changes made inside given subdirectory should be reported.
+ """
+
+ watch_root = dconf_watch('/')
+ watch_org = dconf_watch('/org/')
+
+ # Arbitrary delay to give "dconf watch" time to set-up the watch.
+ # In the case this turns out to be problematic, dconf tool could be
+ # changed to produce debug message after `dconf_client_watch_sync`,
+ # so that we could synchronize on its output.
+
+ time.sleep(0.2)
+
+ dconf('write', '/com/a', '1')
+ dconf('write', '/org/b', '2')
+ dconf('write', '/organ/c', '3')
+ dconf('write', '/c', '4')
+
+ # Again, give "dconf watch" some time to pick-up changes.
+
+ time.sleep(0.2)
+
+ watch_root.terminate()
+ watch_org.terminate()
+
+ watch_root.wait()
+ watch_org.wait()
+
+ # Watch for '/' should capture all writes.
+ expected = '''\
+ /com/a
+ 1
+
+ /org/b
+ 2
+
+ /organ/c
+ 3
+
+ /c
+ 4
+
+ '''
+ self.assertEqual(dedent(expected), watch_root.stdout.read())
+
+ # Watch for '/org/' should capture only a subset of all writes:
+ expected = '''\
+ /org/b
+ 2
+
+ '''
+ self.assertEqual(dedent(expected), watch_org.stdout.read())
+
+ def test_dump_load(self):
+ """Checks that output produced with dump can be used with load and
+ vice versa.
+ """
+ keyfile = dedent('''\
+ [/]
+ password='secret'
+
+ [org/editor]
+ window-fullscreen=true
+ window-size=(1024, 768)
+
+ [org/editor/language/c-sharp]
+ tab-width=8
+
+ [org/editor/language/c]
+ tab-width=2
+ ''')
+
+ # Load and dump is identity.
+ dconf('load', '/', input=keyfile)
+ self.assertEqual(dconf('dump', '/').stdout, keyfile)
+
+ # Copy /org/ directory to /com/.
+ keyfile = dconf('dump', '/org/').stdout
+ dconf('load', '/com/', input=keyfile)
+
+ # Verify that /org/ and /com/ are now exactly the same.
+ keyfile_org = dconf('dump', '/org/').stdout
+ keyfile_com = dconf('dump', '/com/').stdout
+ self.assertEqual(keyfile_org, keyfile_com)
+
+ def test_complete(self):
+ """Tests _complete command used internally to implement bash completion.
+
+ Runs completion queries after loading a sample database from key-file.
+ """
+
+ keyfile = dedent('''\
+ [org]
+ calamity=false
+
+ [org/calculator]
+ window-position=(0, 0)
+
+ [org/calendar]
+ window-position=(0, 0)
+
+ [org/history]
+ file0='/tmp/a'
+ file1='/tmp/b'
+ file2='/tmp/c'
+ ''')
+
+ dconf('load', '/', input=keyfile)
+
+ # Empty string is completed to '/'.
+ completions = dconf_complete('', '')
+ self.assertEqual(completions, ['/'])
+ completions = dconf_complete('/', '')
+ self.assertEqual(completions, ['/'])
+
+ # Invalid paths don't return any completions.
+ completions = dconf_complete('', 'foo/')
+ self.assertEqual(completions, [])
+ completions = dconf_complete('/', 'foo/')
+ self.assertEqual(completions, [])
+
+ # Key completions include trailing whitespace,
+ # directory completions do not.
+ completions = dconf_complete('', '/org/')
+ self.assertEqual(completions,
+ ['/org/calamity ',
+ '/org/calculator/',
+ '/org/calendar/',
+ '/org/history/'])
+
+ # Only matches with given prefix are returned.
+ completions = dconf_complete('', '/org/cal')
+ self.assertEqual(completions,
+ ['/org/calamity ',
+ '/org/calculator/',
+ '/org/calendar/'])
+
+ # Only matches with given suffix are returned.
+ completions = dconf_complete('/', '/org/cal')
+ self.assertEqual(completions,
+ ['/org/calculator/',
+ '/org/calendar/'])
+
+ def test_compile_precedence(self):
+ """Compile processes key-files in reverse lexicographical order.
+
+ When key occurs in multiple files, the value from file processed first
+ is preferred.
+
+ Test that by preparing four key-files each with a different value for
+ '/org/file'. Compiling it directly into user database, and performing
+ read to check which value had been selected.
+ """
+ # Prepare key file database directory.
+ user_d = os.path.join(self.temporary_dir.name, 'user.d')
+ os.mkdir(user_d, mode=0o700)
+
+ def write_config_d(name):
+ keyfile = dedent('''
+ [org]
+ file = {name}
+ '''.format(name=name))
+
+ with open(os.path.join(user_d, name), 'w') as file:
+ file.write(keyfile)
+
+ write_config_d('00')
+ write_config_d('25')
+ write_config_d('50')
+ write_config_d('99')
+
+ # Compile directly into user configuration file.
+ dconf('compile',
+ os.path.join(self.config_home, 'dconf', 'user'),
+ user_d)
+
+ # Lexicographically last value should win:
+ self.assertEqual(dconf_read('/org/file'), '99')
+
+ @unittest.expectedFailure
+ def test_redundant_disk_writes(self):
+ """Redundant disk writes are avoided.
+
+ When write or reset operations don't modify actual contents of the
+ database, the database file shouldn't be needlessly rewritten. Check
+ mtime after each redundant operation to verify that.
+ """
+
+ config = os.path.join(self.config_home, 'dconf', 'user')
+
+ def move_time_back(path):
+ """Moves file mtime 60 seconds back and returns its new value.
+
+ Used to avoid false positives during comparison checks in the case
+ that mtime is stored with low precision.
+ """
+ atime = os.path.getatime(config)
+ mtime = os.path.getmtime(config)
+
+ os.utime(config, times=(atime, mtime - 60))
+
+ return os.path.getmtime(config)
+
+ # Activate service to trigger initial database write.
+ dconf_write('/prime', '5')
+
+ # Sanity check that database is rewritten when necessary.
+ saved_mtime = move_time_back(config)
+ dconf_write('/prime', '13')
+ self.assertLess(saved_mtime, os.path.getmtime(config))
+
+ # Write the same value as one already in the database.
+ saved_mtime = move_time_back(config)
+ dconf('write', '/prime', '13')
+ self.assertEqual(saved_mtime, os.path.getmtime(config))
+
+ # Reset not directory which is not present in the database.
+ saved_mtime = move_time_back(config)
+ dconf('reset', '-f', '/non-existing/directory/')
+ self.assertEqual(saved_mtime, os.path.getmtime(config))
+
+ def test_compile_dotfiles(self):
+ """Compile ignores files starting with a dot."""
+
+ user_d = os.path.join(self.temporary_dir.name, 'user.d')
+ os.mkdir(user_d)
+
+ a_conf = dedent('''\
+ [math]
+ a=42
+ ''')
+
+ a_conf_swp = dedent('''\
+ [math]
+ b=13
+ ''')
+
+ with open(os.path.join(user_d, 'a.conf'), 'w') as file:
+ file.write(a_conf)
+
+ with open(os.path.join(user_d, '.a.conf.swp'), 'w') as file:
+ file.write(a_conf_swp)
+
+ dconf('compile',
+ os.path.join(self.config_home, 'dconf', 'user'),
+ user_d)
+
+ self.assertEqual(a_conf, dconf('dump', '/').stdout)
+
+ def test_database_invalidation(self):
+ """Update invalidates previous database by overwriting the header with
+ null bytes.
+ """
+
+ db = os.path.join(self.temporary_dir.name, 'db')
+ local = os.path.join(db, 'local')
+ local_d = os.path.join(db, 'local.d')
+
+ os.makedirs(local_d)
+
+ with open(os.path.join(local_d, 'local.conf'), 'w') as file:
+ file.write(dedent('''\
+ [org/gnome/desktop/background]
+ picture-uri = 'file:///usr/share/backgrounds/gnome/ColdWarm.jpg'
+ '''))
+
+ # Compile database for the first time.
+ dconf('update', db)
+
+ with open(local, 'rb') as file:
+ with mmap.mmap(file.fileno(), 8, mmap.MAP_SHARED, prot=mmap.PROT_READ) as mm:
+ # Sanity check that database is valid.
+ self.assertNotEqual(b'\0'*8, mm[:8])
+
+ dconf('update', db)
+
+ # Now database should be marked as invalid.
+ self.assertEqual(b'\0'*8, mm[:8])
+
+ def test_update_failure(self):
+ """Update should skip invalid configuration directory and continue with
+ others. Failure to update one of databases should be indicated with
+ non-zero exit code.
+
+ Regression test for issue #42.
+ """
+
+ # A few different scenarios when loading data from key-file:
+ valid_key_file = '[org]\na = 1'
+
+ invalid_key_file = "<html>This isn't a key-file nor valid HTML."
+
+ invalid_group_name = dedent('''\
+ [org//no/me]
+ a = 2
+ ''')
+
+ invalid_key_name = dedent('''\
+ [org/gnome]
+ b// = 2
+ ''')
+
+ invalid_value = dedent('''\
+ [org/gnome]
+ c = 2x2
+ ''')
+
+ db = os.path.join(self.temporary_dir.name, 'db')
+
+ # Database name, valid, content
+ cases = [('site_aa', True, valid_key_file),
+ ('site_bb', False, invalid_key_file),
+ ('site_cc', False, invalid_group_name),
+ ('site_dd', False, invalid_key_name),
+ ('site_ee', False, invalid_value),
+ ('site_ff', True, valid_key_file)]
+
+ for (name, is_valid, content) in cases:
+ conf_dir = os.path.join(db, '{}.d'.format(name))
+ conf_file = os.path.join(conf_dir, '{}.conf'.format(name))
+
+ os.makedirs(conf_dir)
+
+ with open(conf_file, 'w') as file:
+ file.write(content)
+
+ # Return code should indicate failure.
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('update', db, stderr=subprocess.PIPE)
+
+ for (name, is_valid, content) in cases:
+ path = os.path.join(db, name)
+ if is_valid:
+ # This one was valid so db should be written successfully.
+ self.assertTrue(os.path.exists(path))
+ self.assertNotRegex(cm.exception.stderr, name)
+ else:
+ # This one was broken so we shouldn't create corresponding db.
+ self.assertFalse(os.path.exists(path))
+ self.assertRegex(cm.exception.stderr, name)
+
+ def test_locks(self):
+ """Key paths can be locked in system databases.
+
+ - Update configures locks based on files found in "locks" subdirectory.
+ - Locks can be listed with list-locks command.
+ - Locks are enforced during write.
+ - Load can ignore changes to locked keys using -f option.
+ """
+
+ db = os.path.join(self.temporary_dir.name, 'db')
+ profile = os.path.join(self.temporary_dir.name, 'profile')
+ site = os.path.join(db, 'site')
+ site_d = os.path.join(db, 'site.d')
+ site_locks = os.path.join(db, site_d, 'locks')
+
+ os.makedirs(site_locks)
+
+ # For meaningful test of locks we need two sources, first of which
+ # should be writable. We will use user-db and file-db.
+ with open(profile, 'w') as file:
+ file.write(dedent('''\
+ user-db:user
+ file-db:{}
+ '''.format(site)))
+
+ # Environment to use for all dconf client invocations.
+ env = dict(os.environ)
+ env['DCONF_PROFILE'] = profile
+
+ # Default settings
+ with open(os.path.join(site_d, '10-site-defaults'), 'w') as file:
+ file.write(dedent('''\
+ # Some useful default settings for our site
+ [system/proxy/http]
+ host='172.16.0.1'
+ enabled=true
+
+ [org/gnome/desktop]
+ background='company-wallpaper.jpeg'
+ '''))
+
+ # Lock proxy settings.
+ with open(os.path.join(site_locks, '10-proxy-lock'), 'w') as file:
+ file.write(dedent('''\
+ # Prevent changes to proxy
+ /system/proxy/http/host
+ /system/proxy/http/enabled
+ /system/proxy/ftp/host
+ /system/proxy/ftp/enabled
+ '''))
+
+ # Compile site configuration.
+ dconf('update', db)
+
+ # Test list-locks:
+ self.assertEqual(['/system/proxy/ftp/enabled',
+ '/system/proxy/ftp/host',
+ '/system/proxy/http/enabled',
+ '/system/proxy/http/host'],
+ dconf_locks('/', env=env))
+
+ self.assertEqual(['/system/proxy/http/enabled',
+ '/system/proxy/http/host'],
+ dconf_locks('/system/proxy/http/', env=env))
+
+ self.assertEqual([],
+ dconf_locks('/org/gnome/', env=env))
+
+ # Changing unlocked defaults is fine.
+ dconf('write', '/org/gnome/desktop/background',
+ '"ColdWarm.jpg"', env=env)
+
+ # It is an error to change locked keys.
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('write', '/system/proxy/http/enabled', 'false',
+ env=env, stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, 'non-writable keys')
+
+ keyfile = dedent('''\
+ [system/proxy/http]
+ enabled=false
+ [org/gnome/desktop]
+ background='Winter.png'
+ ''')
+
+ # Load fails to apply changes if some key is locked ...
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('load', '/', input=keyfile, env=env, stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, 'non-writable keys')
+ self.assertEqual('true', dconf_read('/system/proxy/http/enabled', env=env))
+ self.assertEqual("'ColdWarm.jpg'", dconf_read('/org/gnome/desktop/background', env=env))
+
+ # ..., unless invoked with -f option, then it changes unlocked keys.
+ stderr = dconf('load', '-f', '/', input=keyfile, env=env, stderr=subprocess.PIPE).stderr
+ self.assertRegex(stderr, 'ignored non-writable key')
+ self.assertEqual('true', dconf_read('/system/proxy/http/enabled', env=env))
+ self.assertEqual("'Winter.png'", dconf_read('/org/gnome/desktop/background', env=env))
+
+ def test_dconf_blame(self):
+ """Blame returns recorded information about write operations.
+
+ Recorded information include sender bus name, sender process id and
+ object path the write operations was invoked on.
+ """
+
+ p = subprocess.Popen([dconf_exe, 'write', '/prime', '307'])
+ p.wait()
+
+ blame = dconf('blame').stdout
+ print(blame)
+
+ self.assertRegex(blame, 'Sender: ')
+ self.assertRegex(blame, 'PID: {}'.format(p.pid))
+ self.assertRegex(blame, 'Object path: /ca/desrt/dconf/Writer/user')
+
+if __name__ == '__main__':
+ # Make sure we don't pick up mandatory profile.
+ mandatory_profile = '/run/dconf/user/{}'.format(os.getuid())
+ assert not os.path.isfile(mandatory_profile)
+
+ # Avoid profile sourced from environment or system data dirs.
+ os.environ.pop('DCONF_PROFILE', None)
+ os.environ.pop('XDG_DATA_DIRS', None)
+ # Avoid interfering with external message buses.
+ os.environ['DBUS_SYSTEM_BUS_ADDRESS'] = ''
+ os.environ['DBUS_SESSION_BUS_ADDRESS'] = ''
+
+ if len(sys.argv) < 3:
+ message = 'Usage: {} path-to-dconf path-to-dconf-service'.format(
+ sys.argv[0])
+ raise RuntimeError(message)
+
+ dconf_exe, dconf_service_exe = sys.argv[1:3]
+ del sys.argv[1:3]
+
+ # Run tests!
+ unittest.main()
diff --git a/tests/tmpdir.c b/tests/tmpdir.c
new file mode 100644
index 0000000..b8c8745
--- /dev/null
+++ b/tests/tmpdir.c
@@ -0,0 +1,53 @@
+#include "tmpdir.h"
+
+#include <glib/gstdio.h>
+#include "../common/dconf-paths.h"
+#include <string.h>
+
+gchar *
+dconf_test_create_tmpdir (void)
+{
+ GError *error = NULL;
+ gchar *temp;
+
+ temp = g_dir_make_tmp ("dconf-testcase.XXXXXX", &error);
+ g_assert_no_error (error);
+ g_assert (temp != NULL);
+
+ return temp;
+}
+
+static void
+rm_rf (const gchar *file)
+{
+ GDir *dir;
+
+ dir = g_dir_open (file, 0, NULL);
+ if (dir)
+ {
+ const gchar *basename;
+
+ while ((basename = g_dir_read_name (dir)))
+ {
+ gchar *fullname;
+
+ fullname = g_build_filename (file, basename, NULL);
+ rm_rf (fullname);
+ g_free (fullname);
+ }
+
+ g_dir_close (dir);
+ g_rmdir (file);
+ }
+
+ else
+ /* excess paranoia -- only unlink if we're really really sure */
+ if (strstr (file, "/dconf-testcase") && !strstr (file, ".."))
+ g_unlink (file);
+}
+
+void
+dconf_test_remove_tmpdir (const gchar *tmpdir)
+{
+ rm_rf (tmpdir);
+}
diff --git a/tests/tmpdir.h b/tests/tmpdir.h
new file mode 100644
index 0000000..6e9dae8
--- /dev/null
+++ b/tests/tmpdir.h
@@ -0,0 +1,9 @@
+#ifndef __dconf_tmpdir_h__
+#define __dconf_tmpdir_h__
+
+#include <glib.h>
+
+gchar *dconf_test_create_tmpdir (void);
+void dconf_test_remove_tmpdir (const gchar *tmpdir);
+
+#endif /* __dconf_tmpdir_h__ */
diff --git a/tests/writer.c b/tests/writer.c
new file mode 100644
index 0000000..955ba91
--- /dev/null
+++ b/tests/writer.c
@@ -0,0 +1,233 @@
+/*
+ * Copyright © 2018 Endless Mobile, Inc
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the licence, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Philip Withnall <withnall@endlessm.com>
+ */
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <locale.h>
+
+#include "service/dconf-generated.h"
+#include "service/dconf-writer.h"
+
+static guint n_warnings = 0;
+
+static GLogWriterOutput
+log_writer_cb (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ if (log_level & G_LOG_LEVEL_WARNING)
+ n_warnings++;
+
+ return G_LOG_WRITER_HANDLED;
+}
+
+static void
+assert_n_warnings (guint expected_n_warnings)
+{
+ g_assert_cmpuint (n_warnings, ==, expected_n_warnings);
+ n_warnings = 0;
+}
+
+typedef struct
+{
+ gchar *dconf_dir; /* (owned) */
+} Fixture;
+
+gchar *config_dir = NULL;
+
+static void
+set_up (Fixture *fixture,
+ gconstpointer test_data)
+{
+ fixture->dconf_dir = g_build_filename (config_dir, "dconf", NULL);
+ g_assert_cmpint (g_mkdir (fixture->dconf_dir, 0755), ==, 0);
+
+ g_test_message ("Using dconf directory: %s", fixture->dconf_dir);
+}
+
+static void
+tear_down (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_assert_cmpint (g_rmdir (fixture->dconf_dir), ==, 0);
+ g_clear_pointer (&fixture->dconf_dir, g_free);
+
+ assert_n_warnings (0);
+}
+
+/* Test basic initialisation of a #DConfWriter. This is essentially a smoketest. */
+static void
+test_writer_basic (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(DConfWriter) writer = NULL;
+
+ writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_WRITER, "some-name"));
+ g_assert_nonnull (writer);
+
+ g_assert_cmpstr (dconf_writer_get_name (writer), ==, "some-name");
+}
+
+/* Test that beginning a write operation when no database exists succeeds. Note
+ * that the database will not actually be created until some changes are made
+ * and the write is committed. */
+static void
+test_writer_begin_missing (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(DConfWriter) writer = NULL;
+ DConfWriterClass *writer_class;
+ gboolean retval;
+ g_autoptr(GError) local_error = NULL;
+ g_autofree gchar *db_filename = g_build_filename (fixture->dconf_dir, "missing", NULL);
+
+ /* Check the database doesn’t exist. */
+ g_assert_false (g_file_test (db_filename, G_FILE_TEST_EXISTS));
+
+ /* Create a writer. */
+ writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_WRITER, "missing"));
+ g_assert_nonnull (writer);
+
+ writer_class = DCONF_WRITER_GET_CLASS (writer);
+ retval = writer_class->begin (writer, &local_error);
+ g_assert_no_error (local_error);
+ g_assert_true (retval);
+}
+
+/* Test that beginning a write operation when a corrupt or empty database exists
+ * will take a backup of the database and then succeed. Note that a new empty
+ * database will not actually be created until some changes are made and the
+ * write is committed. */
+typedef struct
+{
+ const gchar *corrupt_db_contents;
+ guint n_existing_backups;
+} BeginCorruptFileData;
+
+static void
+test_writer_begin_corrupt_file (Fixture *fixture,
+ gconstpointer test_data)
+{
+ const BeginCorruptFileData *data = test_data;
+ g_autoptr(DConfWriter) writer = NULL;
+ DConfWriterClass *writer_class;
+ gboolean retval;
+ g_autoptr(GError) local_error = NULL;
+ g_autofree gchar *db_filename = g_build_filename (fixture->dconf_dir, "corrupt", NULL);
+ g_autofree gchar *new_db_filename_backup = NULL;
+ g_autofree gchar *backup_file_contents = NULL;
+ gsize backup_file_contents_len = 0;
+ guint i;
+
+ /* Create a corrupt database. */
+ g_file_set_contents (db_filename, data->corrupt_db_contents, -1, &local_error);
+ g_assert_no_error (local_error);
+
+ /* Create any existing backups, to test we don’t overwrite them. */
+ for (i = 0; i < data->n_existing_backups; i++)
+ {
+ g_autofree gchar *db_filename_backup = g_strdup_printf ("%s~%u", db_filename, i);
+ g_file_set_contents (db_filename_backup, "backup", -1, &local_error);
+ g_assert_no_error (local_error);
+ }
+
+ new_db_filename_backup = g_strdup_printf ("%s~%u", db_filename, data->n_existing_backups);
+
+ /* Create a writer. */
+ writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_WRITER, "corrupt"));
+ g_assert_nonnull (writer);
+
+ writer_class = DCONF_WRITER_GET_CLASS (writer);
+ retval = writer_class->begin (writer, &local_error);
+ g_assert_no_error (local_error);
+ g_assert_true (retval);
+
+ /* The writer should have printed a warning about the corrupt database. */
+ assert_n_warnings (1);
+
+ /* Check a backup file has been created and has the right content. */
+ g_file_get_contents (new_db_filename_backup, &backup_file_contents,
+ &backup_file_contents_len, &local_error);
+ g_assert_no_error (local_error);
+ g_assert_cmpstr (backup_file_contents, ==, data->corrupt_db_contents);
+ g_assert_cmpuint (backup_file_contents_len, ==, strlen (data->corrupt_db_contents));
+
+ /* Clean up. */
+ g_assert_cmpint (g_unlink (new_db_filename_backup), ==, 0);
+
+ for (i = 0; i < data->n_existing_backups; i++)
+ {
+ g_autofree gchar *db_filename_backup = g_strdup_printf ("%s~%u", db_filename, i);
+ g_assert_cmpint (g_unlink (db_filename_backup), ==, 0);
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_autoptr(GError) local_error = NULL;
+ int retval;
+ const BeginCorruptFileData empty_data = { "", 0 };
+ const BeginCorruptFileData corrupt_file_data0 = {
+ "secretly not a valid GVDB database 😧", 0
+ };
+ const BeginCorruptFileData corrupt_file_data1 = {
+ "secretly not a valid GVDB database 😧", 1
+ };
+ const BeginCorruptFileData corrupt_file_data2 = {
+ "secretly not a valid GVDB database 😧", 2
+ };
+
+ setlocale (LC_ALL, "");
+
+ g_test_init (&argc, &argv, NULL);
+
+ /* Set up a fake $XDG_CONFIG_HOME. We can’t do this in the fixture, as
+ * g_get_user_config_dir() caches its return value. */
+ config_dir = g_dir_make_tmp ("dconf-test-writer_XXXXXX", &local_error);
+ g_assert_no_error (local_error);
+ g_assert_true (g_setenv ("XDG_CONFIG_HOME", config_dir, TRUE));
+ g_test_message ("Using config directory: %s", config_dir);
+
+ /* Log handling so we don’t abort on the first g_warning(). */
+ g_log_set_writer_func (log_writer_cb, NULL, NULL);
+
+ g_test_add ("/writer/basic", Fixture, NULL, set_up,
+ test_writer_basic, tear_down);
+ g_test_add ("/writer/begin/missing", Fixture, NULL, set_up,
+ test_writer_begin_missing, tear_down);
+ g_test_add ("/writer/begin/empty", Fixture, &empty_data, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+ g_test_add ("/writer/begin/corrupt-file/0", Fixture, &corrupt_file_data0, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+ g_test_add ("/writer/begin/corrupt-file/1", Fixture, &corrupt_file_data1, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+ g_test_add ("/writer/begin/corrupt-file/2", Fixture, &corrupt_file_data2, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+
+ retval = g_test_run ();
+
+ /* Clean up the config dir. */
+ g_unsetenv ("XDG_CONFIG_HOME");
+ g_assert_cmpint (g_rmdir (config_dir), ==, 0);
+ g_clear_pointer (&config_dir, g_free);
+
+ return retval;
+}