[gupnp] tests: Add ServiceProxy test



commit 17943f1ba17d192e867fc819b7a42b6982b2dee4
Author: Jens Georg <mail jensge org>
Date:   Sun Jan 23 16:13:03 2022 +0100

    tests: Add ServiceProxy test

 libgupnp/gupnp-service-action.c  |   2 +-
 libgupnp/gupnp-service-private.h |   3 +
 tests/meson.build                |   2 +-
 tests/test-service-proxy.c       | 598 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 603 insertions(+), 2 deletions(-)
---
diff --git a/libgupnp/gupnp-service-action.c b/libgupnp/gupnp-service-action.c
index a8c75d1..6afec90 100644
--- a/libgupnp/gupnp-service-action.c
+++ b/libgupnp/gupnp-service-action.c
@@ -38,7 +38,7 @@ action_dispose (GUPnPServiceAction *action)
 }
 
 void
-gupnp_service_action_unref (GUPnPServiceAction *action)
+gupnp_service_action_unref (struct _GUPnPServiceAction *action)
 {
         g_return_if_fail (action);
 
diff --git a/libgupnp/gupnp-service-private.h b/libgupnp/gupnp-service-private.h
index 5f020d7..0f266c5 100644
--- a/libgupnp/gupnp-service-private.h
+++ b/libgupnp/gupnp-service-private.h
@@ -30,4 +30,7 @@ struct _GUPnPServiceAction {
         guint         argument_count;
 };
 
+void
+gupnp_service_action_unref (struct _GUPnPServiceAction *action);
+
 #endif
diff --git a/tests/meson.build b/tests/meson.build
index b976e8a..deae7ba 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,4 +1,4 @@
-foreach program : ['context', 'bugs', 'service', 'acl']
+foreach program : ['context', 'bugs', 'service', 'acl', 'service-proxy']
     test(
         program,
         executable(
diff --git a/tests/test-service-proxy.c b/tests/test-service-proxy.c
new file mode 100644
index 0000000..347ea73
--- /dev/null
+++ b/tests/test-service-proxy.c
@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2021 Jens Georg.
+ *
+ * Author: Jens Georg <mail jensge org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include <config.h>
+
+#include "libgupnp/gupnp.h"
+#include "libgupnp/gupnp-service-private.h"
+
+#include <string.h>
+#include <libsoup/soup.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+static GUPnPContext *
+create_context (const char *localhost, guint16 port, GError **error)
+{
+        return GUPNP_CONTEXT (g_initable_new (GUPNP_TYPE_CONTEXT,
+                                              NULL,
+                                              error,
+                                              "host-ip",
+                                              localhost,
+                                              "port",
+                                              port,
+                                              NULL));
+}
+
+typedef struct {
+        GMainLoop *loop;
+        GUPnPContext *server_context;
+        GUPnPContext *client_context;
+        GUPnPRootDevice *rd;
+        GUPnPServiceInfo *service;
+        GUPnPControlPoint *cp;
+        GUPnPServiceProxy *proxy;
+        gpointer payload;
+} ProxyTestFixture;
+
+static gboolean
+test_on_timeout (gpointer user_data)
+{
+        g_print ("Timeout in %s\n", (const char *) user_data);
+        g_assert_not_reached ();
+
+        return FALSE;
+}
+
+static void
+test_run_loop (GMainLoop *loop, const char *name)
+{
+        guint timeout_id = 0;
+        int timeout = 2;
+
+        const char *timeout_str = g_getenv ("GUPNP_TEST_TIMEOUT");
+        if (timeout_str != NULL) {
+                long t = atol (timeout_str);
+                if (t != 0)
+                        timeout = t;
+        }
+
+        timeout_id = g_timeout_add_seconds (timeout,
+                                            test_on_timeout,
+                                            (gpointer) name);
+        g_main_loop_run (loop);
+        g_source_remove (timeout_id);
+}
+
+static void
+on_proxy_available (G_GNUC_UNUSED GUPnPControlPoint *cp,
+                    GUPnPServiceProxy *proxy,
+                    gpointer user_data)
+{
+        ProxyTestFixture *tf = user_data;
+
+        tf->proxy = g_object_ref (proxy);
+        g_main_loop_quit (tf->loop);
+}
+
+static void
+test_fixture_setup (ProxyTestFixture *tf, gconstpointer user_data)
+{
+        GError *error = NULL;
+
+        tf->loop = g_main_loop_new (NULL, FALSE);
+        g_assert_nonnull (tf->loop);
+
+        // Create server part
+        tf->server_context =
+                create_context ((const char *) user_data, 0, &error);
+        g_assert_nonnull (tf->server_context);
+        g_assert_no_error (error);
+        GUPnPResourceFactory *factory = gupnp_resource_factory_new ();
+        tf->rd = gupnp_root_device_new_full (tf->server_context,
+                                             factory,
+                                             NULL,
+                                             "TestDevice.xml",
+                                             DATA_PATH,
+                                             &error);
+        g_object_unref (factory);
+        g_assert_no_error (error);
+        g_assert_nonnull (tf->rd);
+        tf->service = gupnp_device_info_get_service (
+                GUPNP_DEVICE_INFO (tf->rd),
+                "urn:test-gupnp-org:service:TestService:1");
+
+        // Create client part
+        tf->client_context =
+                create_context ((const char *) user_data, 0, &error);
+
+        g_assert_nonnull (tf->client_context);
+        g_assert_no_error (error);
+        tf->cp = gupnp_control_point_new (
+                tf->client_context,
+                "urn:test-gupnp-org:service:TestService:1");
+
+        gulong id = g_signal_connect (tf->cp,
+                                      "service-proxy-available",
+                                      G_CALLBACK (on_proxy_available),
+                                      tf);
+        gupnp_root_device_set_available (tf->rd, TRUE);
+        gssdp_resource_browser_set_active (GSSDP_RESOURCE_BROWSER (tf->cp),
+                                           TRUE);
+        test_run_loop (tf->loop, "Test fixture setup");
+        g_signal_handler_disconnect (tf->cp, id);
+}
+
+static gboolean
+delayed_loop_quitter (gpointer user_data)
+{
+        g_main_loop_quit (user_data);
+        return G_SOURCE_REMOVE;
+}
+
+static void
+test_fixture_teardown (ProxyTestFixture *tf, gconstpointer user_data)
+{
+        g_clear_object (&tf->proxy);
+        g_object_unref (tf->cp);
+        g_object_unref (tf->client_context);
+
+        g_object_unref (tf->service);
+        g_object_unref (tf->rd);
+        g_object_unref (tf->server_context);
+
+        // Make sure the source teardown handlers get run so we don't confuse valgrind
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+        g_main_loop_unref (tf->loop);
+}
+
+// Test that calls a remote function and does not even connect to any callback
+// Is is mainly useful to check in combination with ASAN/Valgrind to make sure
+// that nothing gets leaked on the way
+void
+test_fire_and_forget (ProxyTestFixture *tf,
+                      G_GNUC_UNUSED gconstpointer user_data)
+{
+        // Run fire and forget for action that does not have any kind of arguments
+        GUPnPServiceProxyAction *action =
+                gupnp_service_proxy_action_new ("Ping", NULL);
+
+        gupnp_service_proxy_call_action_async (tf->proxy,
+                                               action,
+                                               NULL,
+                                               NULL,
+                                               NULL);
+        gupnp_service_proxy_action_unref (action);
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+
+        // Run fire and forget for a more complex action
+        action = gupnp_service_proxy_action_new ("Browse",
+                                                 "ObjectID",
+                                                 G_TYPE_STRING,
+                                                 "0",
+                                                 "BrowseFlag",
+                                                 G_TYPE_STRING,
+                                                 "BrowseDirectChildren",
+                                                 "Filter",
+                                                 G_TYPE_STRING,
+                                                 "res,dc:date,res@size",
+                                                 "StartingIndex",
+                                                 G_TYPE_UINT,
+                                                 0,
+                                                 "RequestedCount",
+                                                 G_TYPE_UINT,
+                                                 0,
+                                                 "SortCriteria",
+                                                 G_TYPE_STRING,
+                                                 "",
+                                                 NULL);
+
+        gupnp_service_proxy_call_action_async (tf->proxy,
+                                               action,
+                                               NULL,
+                                               NULL,
+                                               NULL);
+        gupnp_service_proxy_action_unref (action);
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+}
+
+void
+on_test_async_call_ping_success (G_GNUC_UNUSED GUPnPService *service,
+                                 G_GNUC_UNUSED GUPnPServiceAction *action,
+                                 G_GNUC_UNUSED gpointer user_data)
+{
+        gupnp_service_action_return_success (action);
+}
+
+void
+on_test_async_call (GObject *source, GAsyncResult *res, gpointer user_data)
+{
+        GError *error = NULL;
+        g_assert_no_error (error);
+        g_assert_nonnull (user_data);
+
+        gupnp_service_proxy_call_action_finish (GUPNP_SERVICE_PROXY (source),
+                                                res,
+                                                &error);
+
+        ProxyTestFixture *tf = (ProxyTestFixture *) user_data;
+        g_main_loop_quit (tf->loop);
+}
+
+void
+test_async_call (ProxyTestFixture *tf, G_GNUC_UNUSED gconstpointer user_data)
+{
+        // Run fire and forget for action that does not have any kind of arguments
+        GUPnPServiceProxyAction *action =
+                gupnp_service_proxy_action_new ("Ping", NULL);
+
+        g_signal_connect (tf->service,
+                          "action-invoked::Ping",
+                          G_CALLBACK (on_test_async_call_ping_success),
+                          tf);
+
+        gupnp_service_proxy_call_action_async (tf->proxy,
+                                               action,
+                                               NULL,
+                                               on_test_async_call,
+                                               tf);
+        gupnp_service_proxy_action_unref (action);
+        test_run_loop(tf->loop, g_test_get_path());
+
+        // Run fire and forget for a more complex action
+        action = gupnp_service_proxy_action_new ("Browse",
+                                                 "ObjectID",
+                                                 G_TYPE_STRING,
+                                                 "0",
+                                                 "BrowseFlag",
+                                                 G_TYPE_STRING,
+                                                 "BrowseDirectChildren",
+                                                 "Filter",
+                                                 G_TYPE_STRING,
+                                                 "res,dc:date,res@size",
+                                                 "StartingIndex",
+                                                 G_TYPE_UINT,
+                                                 0,
+                                                 "RequestedCount",
+                                                 G_TYPE_UINT,
+                                                 0,
+                                                 "SortCriteria",
+                                                 G_TYPE_STRING,
+                                                 "",
+                                                 NULL);
+
+        gupnp_service_proxy_call_action_async (tf->proxy,
+                                               action,
+                                               NULL,
+                                               on_test_async_call,
+                                               tf);
+        gupnp_service_proxy_action_unref (action);
+        test_run_loop (tf->loop, g_test_get_path ());
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+}
+
+void
+on_test_async_call_ping_delay (G_GNUC_UNUSED GUPnPService *service,
+                               G_GNUC_UNUSED GUPnPServiceAction *action,
+                               gpointer user_data)
+{
+        g_debug ("=> Ping delay");
+        ProxyTestFixture *tf = (ProxyTestFixture *) user_data;
+        tf->payload = action;
+        g_main_loop_quit (tf->loop);
+}
+
+void
+on_test_async_cancel_call (GObject *source,
+                           GAsyncResult *res,
+                           gpointer user_data)
+{
+        GError *error = NULL;
+        g_assert_nonnull (user_data);
+
+        gupnp_service_proxy_call_action_finish (GUPNP_SERVICE_PROXY (source),
+                                                res,
+                                                &error);
+
+        g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+        g_clear_error (&error);
+
+        ProxyTestFixture *tf = (ProxyTestFixture *) user_data;
+        g_main_loop_quit (tf->loop);
+}
+
+void
+test_async_cancel_call (ProxyTestFixture *tf,
+                        G_GNUC_UNUSED gconstpointer user_data)
+{
+        GUPnPServiceProxyAction *action =
+                gupnp_service_proxy_action_new ("Ping", NULL);
+
+        g_signal_connect (tf->service,
+                          "action-invoked::Ping",
+                          G_CALLBACK (on_test_async_call_ping_delay),
+                          tf);
+
+        GCancellable *cancellable = g_cancellable_new ();
+        gupnp_service_proxy_call_action_async (tf->proxy,
+                                               action,
+                                               cancellable,
+                                               on_test_async_cancel_call,
+                                               tf);
+
+        // This should be called by the action callback
+        test_run_loop (tf->loop, g_test_get_path ());
+        g_cancellable_cancel (cancellable);
+
+        // This should be finished by the now-cancelled proxy call
+        test_run_loop (tf->loop, g_test_get_path ());
+
+        // Free action. There should not be any callback
+        gupnp_service_action_return_success (
+                (GUPnPServiceAction *) tf->payload);
+
+        GError *error = NULL;
+        gupnp_service_proxy_action_get_result (action, &error, NULL);
+        g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+        g_clear_error (&error);
+
+        gupnp_service_proxy_action_unref (action);
+        g_object_unref (cancellable);
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+}
+
+
+void
+test_async_call_destroy_with_pending (ProxyTestFixture *tf,
+                                      gconstpointer user_data)
+{
+
+        GList *actions = NULL;
+        // Since the session in the context is using defaults, we can only have
+        // two concurrent connections on a remote host
+        for (int i = 0; i < 2; i++) {
+                GUPnPServiceProxyAction *action =
+                        gupnp_service_proxy_action_new ("Ping", NULL);
+
+                gupnp_service_proxy_call_action_async (tf->proxy,
+                                                       action,
+                                                       NULL,
+                                                       NULL,
+                                                       NULL);
+                gupnp_service_proxy_action_unref (action);
+
+                // This should be called by the action callback
+                gulong id = g_signal_connect (
+                        tf->service,
+                        "action-invoked::Ping",
+                        G_CALLBACK (on_test_async_call_ping_delay),
+                        tf);
+                test_run_loop (tf->loop, g_test_get_path ());
+                g_signal_handler_disconnect (tf->service, id);
+
+                actions = g_list_prepend (actions, tf->payload);
+        }
+
+        // free the actions
+        g_list_free_full (actions, (GDestroyNotify) gupnp_service_action_unref);
+
+        g_clear_object (&tf->proxy);
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+}
+
+typedef struct {
+        GMainLoop *outer_loop;
+        GMainContext *outer_context;
+        const char *address;
+        GCancellable *cancellable;
+} ThreadData;
+
+typedef struct {
+        GMainLoop *loop;
+        GUPnPServiceProxy *p;
+} GetProxyData;
+
+void
+thead_on_proxy_available (GUPnPControlPoint *cp,
+                          GUPnPServiceProxy *p,
+                          gpointer user_data)
+{
+        GetProxyData *d = (GetProxyData *) user_data;
+        d->p = g_object_ref (p);
+        g_main_loop_quit (d->loop);
+}
+
+gboolean
+exit_outer_loop (gpointer user_data)
+{
+        ThreadData *d = (ThreadData *) user_data;
+        g_main_loop_quit (d->outer_loop);
+
+        return G_SOURCE_REMOVE;
+}
+
+gpointer
+thread_func (gpointer data)
+{
+        ThreadData *d = (ThreadData *) data;
+        GMainContext *context = g_main_context_new ();
+        GError *error = NULL;
+        g_main_context_push_thread_default (context);
+
+        GUPnPContext *ctx = create_context (d->address, 0, &error);
+        g_assert_no_error (error);
+        g_assert_nonnull (ctx);
+
+        GUPnPControlPoint *cp = gupnp_control_point_new (
+                ctx,
+                "urn:test-gupnp-org:service:TestService:1");
+        GetProxyData gpd;
+        gulong id = g_signal_connect (cp,
+                                      "service-proxy-available",
+                                      G_CALLBACK (thead_on_proxy_available),
+                                      &gpd);
+        gssdp_resource_browser_set_active (GSSDP_RESOURCE_BROWSER (cp), TRUE);
+
+        gpd.loop = g_main_loop_new (context, FALSE);
+        test_run_loop (gpd.loop, "Test thread setup");
+        g_signal_handler_disconnect (cp, id);
+
+        GUPnPServiceProxyAction *action =
+                gupnp_service_proxy_action_new ("Ping", NULL);
+
+        gupnp_service_proxy_call_action (gpd.p, action, d->cancellable, &error);
+        gupnp_service_proxy_action_unref (action);
+
+        if (d->cancellable == NULL)
+                g_assert_no_error (error);
+        else
+                g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+
+        g_object_unref (gpd.p);
+        g_object_unref (cp);
+        g_object_unref (ctx);
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, gpd.loop);
+        g_main_loop_run (gpd.loop);
+
+        g_main_loop_unref (gpd.loop);
+        g_main_context_pop_thread_default (context);
+        g_main_context_unref (context);
+
+        g_main_context_invoke (d->outer_context, exit_outer_loop, d);
+
+        return NULL;
+}
+
+void
+test_sync_call (ProxyTestFixture *tf, gconstpointer user_data)
+{
+        g_signal_connect (tf->service,
+                          "action-invoked::Ping",
+                          G_CALLBACK (on_test_async_call_ping_success),
+                          tf);
+        ThreadData d;
+        d.address = (const char *) user_data;
+        d.outer_context = g_main_context_get_thread_default ();
+        d.outer_loop = tf->loop;
+        d.cancellable = NULL;
+
+        GThread *t = g_thread_new ("Sync call test", thread_func, &d);
+        test_run_loop (tf->loop, g_test_get_path());
+        g_thread_join (t);
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+}
+
+gboolean
+cancel_sync_call (gpointer user_data)
+{
+        ThreadData *d = (ThreadData *) user_data;
+
+        g_print ("Cancelling...\n");
+        g_cancellable_cancel (d->cancellable);
+
+        return G_SOURCE_REMOVE;
+}
+
+void
+test_cancel_sync_call (ProxyTestFixture *tf, gconstpointer user_data)
+{
+        g_signal_connect (tf->service,
+                          "action-invoked::Ping",
+                          G_CALLBACK (on_test_async_call_ping_delay),
+                          tf);
+        ThreadData d;
+        d.address = (const char *) user_data;
+        d.outer_context = g_main_context_get_thread_default ();
+        d.outer_loop = tf->loop;
+        d.cancellable = g_cancellable_new ();
+
+        GThread *t = g_thread_new ("Sync call cancel test", thread_func, &d);
+        test_run_loop (tf->loop, g_test_get_path());
+
+        g_timeout_add_seconds (1, (GSourceFunc) cancel_sync_call, &d);
+        test_run_loop (tf->loop, g_test_get_path());
+
+        g_thread_join (t);
+
+        // Spin the loop for a bit...
+        g_timeout_add (500, (GSourceFunc) delayed_loop_quitter, tf->loop);
+        g_main_loop_run (tf->loop);
+}
+
+int
+main (int argc, char *argv[])
+{
+        g_test_init (&argc, &argv, NULL);
+
+        g_test_add ("/service-proxy/async/fire-and-forget",
+                    ProxyTestFixture,
+                    "127.0.0.1",
+                    test_fixture_setup,
+                    test_fire_and_forget,
+                    test_fixture_teardown);
+
+        g_test_add ("/service-proxy/async/call",
+                    ProxyTestFixture,
+                    "127.0.0.1",
+                    test_fixture_setup,
+                    test_async_call,
+                    test_fixture_teardown);
+
+        g_test_add ("/service-proxy/async/cancel",
+                    ProxyTestFixture,
+                    "127.0.0.1",
+                    test_fixture_setup,
+                    test_async_cancel_call,
+                    test_fixture_teardown);
+
+        g_test_add ("/service-proxy/async/destroy-with-pending",
+                    ProxyTestFixture,
+                    "127.0.0.1",
+                    test_fixture_setup,
+                    test_async_call_destroy_with_pending,
+                    test_fixture_teardown);
+
+        g_test_add ("/service-proxy/sync/call",
+                    ProxyTestFixture,
+                    "127.0.0.1",
+                    test_fixture_setup,
+                    test_sync_call,
+                    test_fixture_teardown);
+
+        g_test_add ("/service-proxy/sync/cancel-call",
+                    ProxyTestFixture,
+                    "127.0.0.1",
+                    test_fixture_setup,
+                    test_cancel_sync_call,
+                    test_fixture_teardown);
+
+        return g_test_run ();
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]