GtDBusQueue

GtDBusQueue — D-Bus service mock implementation

Stability Level

Unstable, unless otherwise indicated

Functions

Types and Values

Includes

#include <libglib-testing/dbus-queue.h>

Description

GtDBusQueue is an object which allows a D-Bus service to be mocked and implemented to return a variety of results from method calls with a high degree of flexibility. The mock service is driven from within the same process (and potentially the same GMainContext) as the code under test.

This allows a D-Bus service to be mocked without needing to generate and implement a full implementation of its interfaces.

A single GtDBusQueue instance can be used to mock one or more D-Bus services, depending on whether it’s desirable to process the queues of method calls to those services in order or independently from each other. Each GtDBusQueue has a queue of method calls received by the services it is mocking, which are ordered the same as they were received off the bus. It is intended that the test harness which is using GtDBusQueue should pop messages off the queue, and either check they are as expected and return a static reply, or construct a reply dynamically based on their contents.

Messages can be popped off the queue using gt_dbus_queue_assert_pop_message(), gt_dbus_queue_pop_message() or gt_dbus_queue_try_pop_message(). The former two block until a message can be popped, iterating the thread-default GMainContext while they block. The latter returns FALSE immediately if the queue is empty.

Popping and handling messages is typically done in the GtDBusQueue server thread using gt_dbus_queue_set_server_func(). This will work whether the code under test is synchronous or asynchronous. If the code under test is asynchronous, popping and handling messages can instead be done in the main test thread, but this has no particular advantages.

By default, a GtDBusQueue will not assert that its message queue is empty on destruction unless the assert_queue_empty argument is passed to gt_dbus_queue_disconnect(). If that argument is FALSE, it is highly recommended that gt_dbus_queue_assert_no_messages() is called before a GtDBusQueue is destroyed, or after a particular unit test is completed.

Conversely, a GtDBusQueue will not ensure that the thread default GMainContext for the thread where it’s constructed is empty when the GtDBusQueue is finalised. That is the responsibility of the caller who constructed the GMainContext.

Usage Example with GLib Testing Framework

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
typedef struct
{
  GtDBusQueue *queue;  // (owned)
  uid_t valid_uid;
} BusFixture;

static void
bus_set_up (BusFixture    *fixture,
            gconstpointer  test_data)
{
  g_autoptr(GError) local_error = NULL;
  g_autofree gchar *object_path = NULL;

  fixture->valid_uid = 500;  // arbitrarily chosen
  fixture->queue = gt_dbus_queue_new ();

  gt_dbus_queue_connect (fixture->queue, &local_error);
  g_assert_no_error (local_error);

  gt_dbus_queue_own_name (fixture->queue, "org.freedesktop.Accounts");

  object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", fixture->valid_uid);
  gt_dbus_queue_export_object (fixture->queue,
                               object_path,
                               (GDBusInterfaceInfo *) &app_filter_interface_info,
                               &local_error);
  g_assert_no_error (local_error);

  gt_dbus_queue_export_object (fixture->queue,
                               "/org/freedesktop/Accounts",
                               (GDBusInterfaceInfo *) &accounts_interface_info,
                               &local_error);
  g_assert_no_error (local_error);
}

static void
bus_tear_down (BusFixture    *fixture,
               gconstpointer  test_data)
{
  gt_dbus_queue_disconnect (fixture->queue, TRUE);
  g_clear_pointer (&fixture->queue, gt_dbus_queue_free);
}

// Helper #GAsyncReadyCallback which returns the #GAsyncResult in its @user_data.
static void
async_result_cb (GObject      *obj,
                 GAsyncResult *result,
                 gpointer      user_data)
{
  GAsyncResult **result_out = (GAsyncResult **) user_data;

  g_assert_null (*result_out);
  *result_out = g_object_ref (result);
}

// Test that getting an #EpcAppFilter from the mock D-Bus service works. The
// @test_data is a boolean value indicating whether to do the call
// synchronously (%FALSE) or asynchronously (%TRUE).
//
// The mock D-Bus replies are generated in get_app_filter_server_cb(), which is
// used for both synchronous and asynchronous calls.
static void get_app_filter_server_cb (GtDBusQueue *queue,
                                      gpointer     user_data);

static void
test_app_filter_bus_get (BusFixture    *fixture,
                         gconstpointer  test_data)
{
  g_autoptr(EpcAppFilter) app_filter = NULL;
  g_autoptr(GError) local_error = NULL;
  gboolean test_async = GPOINTER_TO_UINT (test_data);

  gt_dbus_queue_set_server_func (fixture->queue, get_app_filter_server_cb, fixture);

  if (test_async)
    {
      g_autoptr(GAsyncResult) result = NULL;

      epc_get_app_filter_async (gt_dbus_queue_get_client_connection (fixture->queue),
                                fixture->valid_uid,
                                FALSE, NULL, async_result_cb, &result);

      while (result == NULL)
        g_main_context_iteration (NULL, TRUE);
      app_filter = epc_get_app_filter_finish (result, &local_error);
    }
  else
    {
      app_filter = epc_get_app_filter (gt_dbus_queue_get_client_connection (fixture->queue),
                                       fixture->valid_uid,
                                       FALSE, NULL, &local_error);
    }

  g_assert_no_error (local_error);
  g_assert_nonnull (app_filter);

  // Check the app filter properties.
  g_assert_cmpuint (epc_app_filter_get_user_id (app_filter), ==, fixture->valid_uid);
  g_assert_false (epc_app_filter_is_flatpak_app_allowed (app_filter, "org.gnome.Builder"));
  g_assert_true (epc_app_filter_is_flatpak_app_allowed (app_filter, "org.gnome.Chess"));
}

// This is run in a worker thread.
static void
get_app_filter_server_cb (GtDBusQueue *queue,
                          gpointer     user_data)
{
  BusFixture *fixture = user_data;
  g_autoptr(GDBusMethodInvocation) invocation1 = NULL;
  g_autoptr(GDBusMethodInvocation) invocation2 = NULL;
  g_autofree gchar *object_path = NULL;
  g_autofree gchar *reply1 = NULL;

  // Handle the FindUserById() call.
  gint64 user_id;
  invocation1 =
      gt_dbus_queue_assert_pop_message (queue,
                                        "/org/freedesktop/Accounts",
                                        "org.freedesktop.Accounts",
                                        "FindUserById", "(x)", &user_id);
  g_assert_cmpint (user_id, ==, fixture->valid_uid);

  object_path = g_strdup_printf ("/org/freedesktop/Accounts/User%u", (uid_t) user_id);
  reply1 = g_strdup_printf ("(@o '%s',)", object_path);
  g_dbus_method_invocation_return_value (invocation1, g_variant_new_parsed (reply1));

  // Handle the Properties.GetAll() call and return some arbitrary, valid values
  // for the given user.
  const gchar *property_interface;
  invocation2 =
      gt_dbus_queue_assert_pop_message (queue,
                                        object_path,
                                        "org.freedesktop.DBus.Properties",
                                        "GetAll", "(&s)", &property_interface);
  g_assert_cmpstr (property_interface, ==, "com.endlessm.ParentalControls.AppFilter");

  const gchar *reply2 =
    "({"
      "'allow-user-installation': <true>,"
      "'allow-system-installation': <false>,"
      "'app-filter': <(false, ['app/org.gnome.Builder/x86_64/stable'])>,"
      "'oars-filter': <('oars-1.1', { 'violence-bloodshed': 'mild' })>"
    "},)";
  g_dbus_method_invocation_return_value (invocation2, g_variant_new_parsed (reply2));
}

Functions

GtDBusQueueServerFunc ()

void
(*GtDBusQueueServerFunc) (GtDBusQueue *queue,
                          gpointer user_data);

Function called in the server thread to handle incoming method calls. See gt_dbus_queue_set_server_func() for details.

Parameters

queue

a GtDBusQueue

 

user_data

user data passed to gt_dbus_queue_set_server_func()

 

Since: 0.1.0


gt_dbus_queue_new ()

GtDBusQueue *
gt_dbus_queue_new (void);

Create a new GtDBusQueue. Start it using gt_dbus_queue_connect(), own a name using gt_dbus_queue_own_name() and register objects using gt_dbus_queue_export_object(). Start a particular test run using gt_dbus_queue_set_server_func().

Returns

a new GtDBusQueue.

[transfer full]

Since: 0.1.0


gt_dbus_queue_free ()

void
gt_dbus_queue_free (GtDBusQueue *self);

Free a GtDBusQueue. This will call gt_dbus_queue_disconnect() if it hasn’t been called already, and will assert that there are no messages left in the server message queue.

If you wish to free the GtDBusQueue regardless of whether there are messages left in the server message queue, call gt_dbus_queue_disconnect() explicitly before this function, and pass FALSE as its second argument.

Parameters

self

a GtDBusQueue.

[transfer full]

Since: 0.1.0


gt_dbus_queue_get_client_connection ()

GDBusConnection *
gt_dbus_queue_get_client_connection (GtDBusQueue *self);

Get the client GDBusConnection which should be passed to the code under test as its connection to a bus. This will be NULL if gt_dbus_queue_connect() has not been called yet, or if gt_dbus_queue_disconnect() has been called.

Parameters

self

a GtDBusQueue

 

Returns

the client’s bus connection.

[nullable][transfer none]

Since: 0.1.0


gt_dbus_queue_connect ()

gboolean
gt_dbus_queue_connect (GtDBusQueue *self,
                       GError **error);

Create a private bus, mock D-Bus service, and a client GDBusConnection to be used by the code under test. Once this function has been called, the test harness may call gt_dbus_queue_own_name() and gt_dbus_queue_export_object() and then run the code under test.

This must be called from the thread which constructed the GtDBusQueue.

Parameters

self

a GtDBusQueue

 

error

return location for a GError, or NULL

 

Returns

TRUE on success, FALSE otherwise

Since: 0.1.0


gt_dbus_queue_disconnect ()

void
gt_dbus_queue_disconnect (GtDBusQueue *self,
                          gboolean assert_queue_empty);

Disconnect the mock D-Bus service and client GDBusConnection, and shut down the private bus.

This must be called from the thread which constructed the GtDBusQueue.

Parameters

self

a GtDBusQueue

 

assert_queue_empty

TRUE to assert that the server message queue is empty before disconnecting, FALSE to do nothing

 

Since: 0.1.0


gt_dbus_queue_own_name ()

guint
gt_dbus_queue_own_name (GtDBusQueue *self,
                        const gchar *name);

Make the mock D-Bus service acquire the given name on the private bus, so that code under test can address the mock service using name . This behaves similarly to g_bus_own_name().

This may be called from any thread after gt_dbus_queue_connect() has been called.

Parameters

self

a GtDBusQueue

 

name

the well-known D-Bus name to own

 

Returns

ID for the name ownership, which may be passed to gt_dbus_queue_unown_name() to release it in future; guaranteed to be non-zero

Since: 0.1.0


gt_dbus_queue_unown_name ()

void
gt_dbus_queue_unown_name (GtDBusQueue *self,
                          guint id);

Make the mock D-Bus service release a name on the private bus previously acquired using gt_dbus_queue_own_name(). This behaves similarly to g_bus_unown_name().

This may be called from any thread after gt_dbus_queue_connect() has been called.

Parameters

self

a GtDBusQueue

 

id

the name ID returned by gt_dbus_queue_own_name()

 

Since: 0.1.0


gt_dbus_queue_export_object ()

guint
gt_dbus_queue_export_object (GtDBusQueue *self,
                             const gchar *object_path,
                             GDBusInterfaceInfo *interface_info,
                             GError **error);

Make the mock D-Bus service export an interface matching interface_info at the given object_path , so that code under test can call methods at that object_path . This behaves similarly to g_dbus_connection_register_object().

This may be called from any thread after gt_dbus_queue_connect() has been called.

Parameters

self

a GtDBusQueue

 

object_path

the path to export an object on

 

interface_info

definition of the interface to export.

[transfer none]

error

return location for a GError, or NULL

 

Returns

ID for the exported object, which may be passed to gt_dbus_queue_unexport_object() to release it in future; guaranteed to be non-zero

Since: 0.1.0


gt_dbus_queue_unexport_object ()

void
gt_dbus_queue_unexport_object (GtDBusQueue *self,
                               guint id);

Make the mock D-Bus service unexport an object on the private bus previously exported using gt_dbus_queue_export_object(). This behaves similarly to g_dbus_connection_unregister_object().

This may be called from any thread after gt_dbus_queue_connect() has been called.

Parameters

self

a GtDBusQueue

 

id

the name ID returned by gt_dbus_queue_export_object()

 

Since: 0.1.0


gt_dbus_queue_set_server_func ()

void
gt_dbus_queue_set_server_func (GtDBusQueue *self,
                               GtDBusQueueServerFunc func,
                               gpointer user_data);

Set a function to run in the server thread to handle incoming method calls. This is a requirement when testing code which makes synchronous function calls, as they will block the test thread’s main context until they return. This can also be used when testing asynchronous code, which allows reuse of the same mock service implementation when testing synchronous and asynchronous versions of the same code under test functionality.

func will be executed in the server thread, so must only call thread safe methods of the GtDBusQueue, and must use thread safe access to user_data if it’s used in any other threads.

Parameters

self

a GtDBusQueue

 

func

a GtDBusQueueServerFunc to run in the server thread.

[not nullable]

user_data

data to pass to func

 

Since: 0.1.0


gt_dbus_queue_get_n_messages ()

gsize
gt_dbus_queue_get_n_messages (GtDBusQueue *self);

Get the number of messages waiting in the server queue to be popped by gt_dbus_queue_pop_message() and processed.

If asserting that the queue is empty, gt_dbus_queue_assert_no_messages() is more appropriate.

This may be called from any thread.

Parameters

self

a GtDBusQueue

 

Returns

number of messages waiting to be popped and processed

Since: 0.1.0


gt_dbus_queue_try_pop_message ()

gboolean
gt_dbus_queue_try_pop_message (GtDBusQueue *self,
                               GDBusMethodInvocation **out_invocation);

Pop a message off the server’s message queue, if one is ready to be popped. Otherwise, immediately return NULL.

This may be called from any thread after gt_dbus_queue_connect() has been called.

Parameters

self

a GtDBusQueue

 

out_invocation

return location for the popped GDBusMethodInvocation, which may be NULL; pass NULL to not receive the GDBusMethodInvocation.

[out][transfer full][optional][nullable]

Returns

TRUE if a message was popped (and returned in out_invocation if out_invocation was non-NULL), FALSE otherwise

Since: 0.1.0


gt_dbus_queue_pop_message ()

gboolean
gt_dbus_queue_pop_message (GtDBusQueue *self,
                           GDBusMethodInvocation **out_invocation);

Pop a message off the server’s message queue, if one is ready to be popped. Otherwise, block indefinitely until one is.

This may be called from any thread after gt_dbus_queue_connect() has been called.

Parameters

self

a GtDBusQueue

 

out_invocation

return location for the popped GDBusMethodInvocation, which may be NULL; pass NULL to not receive the GDBusMethodInvocation.

[out][transfer full][optional][nullable]

Returns

TRUE if a message was popped (and returned in out_invocation if out_invocation was non-NULL), FALSE if the pop timed out

Since: 0.1.0


gt_dbus_queue_match_client_message ()

gboolean
gt_dbus_queue_match_client_message (GtDBusQueue *self,
                                    GDBusMethodInvocation *invocation,
                                    const gchar *expected_object_path,
                                    const gchar *expected_interface_name,
                                    const gchar *expected_method_name,
                                    const gchar *expected_parameters_string);

Check whether invocation matches the given expected object path, interface name, method name and (optionally) parameters, and was sent by the client connection of the GtDBusQueue.

This may be called from any thread after gt_dbus_queue_connect() has been called.

expected_parameters_string is optional, and will be matched against only if it is non-NULL. The other arguments are not optional. If non-NULL, expected_parameters_string will be parsed using g_variant_new_parsed(). It is a programmer error to provide a string which doesn’t parse correctly.

Parameters

self

a GtDBusQueue

 

invocation

invocation to match against.

[transfer none]

expected_object_path

object path the invocation is expected to be calling

 

expected_interface_name

interface name the invocation is expected to be calling

 

expected_method_name

method name the invocation is expected to be calling

 

expected_parameters_string

expected parameters for the invocation, or NULL to not match its parameters.

[nullable]

Returns

TRUE if invocation matches the expected arguments, FALSE otherwise

Since: 0.1.0


gt_dbus_queue_format_message ()

gchar *
gt_dbus_queue_format_message (GDBusMethodInvocation *invocation);

Format a GDBusMethodInvocation in a human readable way. This format is not intended to be stable or machine parsable.

Parameters

invocation

a GDBusMethodInvocation to format

 

Returns

human readable version of invocation .

[transfer full]

Since: 0.1.0


gt_dbus_queue_format_messages ()

gchar *
gt_dbus_queue_format_messages (GtDBusQueue *self);

Format all the messages currently pending in the mock service’s message queue in a human readable way, with the head of the queue first in the formatted list. This format is not intended to be stable or machine parsable.

If no messages are in the queue, an empty string will be returned.

Parameters

self

a GtDBusQueue

 

Returns

human readable version of the pending message queue.

[transfer full]

Since: 0.1.0


gt_dbus_queue_assert_no_messages()

#define             gt_dbus_queue_assert_no_messages(self)

Assert that there are no messages currently in the mock service’s message queue.

If there are, an assertion fails and some debug output is printed.

Parameters

self

a GtDBusQueue

 

Since: 0.1.0


gt_dbus_queue_assert_pop_message()

#define             gt_dbus_queue_assert_pop_message(self, expected_object_path, expected_interface_name, expected_method_name, parameters_format, ...)

Assert that a message can be popped off the mock service’s message queue (using gt_dbus_queue_pop_message(), which will block) and that it is a method call from the GtDBusQueue’s client connection to the mock service, calling expected_method_name on expected_interface_name at expected_object_path (as determined using gt_dbus_queue_match_client_message() with a NULL parameters argument). The parameters in the method call will be returned in the return locations given in the varargs, according to the parameters_format , using g_variant_get_va().

If a timeout occurs when popping a message, or if the popped message doesn’t match the expected object path, interface name or method name, an assertion fails and some debug output is printed.

Parameters

self

a GtDBusQueue

 

expected_object_path

object path the invocation is expected to be calling

 

expected_interface_name

interface name the invocation is expected to be calling

 

expected_method_name

method name the invocation is expected to be calling

 

parameters_format

g_variant_get() format string to extract the parameters from the popped GDBusMethodInvocation into the return locations provided in @...

 

...

return locations for the parameter placeholders given in parameters_format

 

Returns

the popped GDBusMethodInvocation.

[transfer full]

Since: 0.1.0

Types and Values

GtDBusQueue

typedef struct _GtDBusQueue GtDBusQueue;

An object which allows a D-Bus service to be mocked and implemented to return a variety of results from method calls with a high degree of flexibility. The mock service is driven from within the same process (and potentially the same GMainContext) as the code under test.

This allows a D-Bus service to be mocked without needing to generate and implement a full implementation of its interfaces.

Since: 0.1.0