summaryrefslogtreecommitdiff
path: root/tests/engine.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/engine.c')
-rw-r--r--tests/engine.c2100
1 files changed, 2100 insertions, 0 deletions
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;
+}