diff options
author | Jens Georg <mail@jensge.org> | 2022-01-23 16:13:03 +0100 |
---|---|---|
committer | Jens Georg <mail@jensge.org> | 2022-01-23 16:13:03 +0100 |
commit | 17943f1ba17d192e867fc819b7a42b6982b2dee4 (patch) | |
tree | 1e3a781970bba00716d654ffa220e306041f016c /tests | |
parent | a23561d3e17f0d44aeda79495083220e95951aa8 (diff) | |
download | gupnp-17943f1ba17d192e867fc819b7a42b6982b2dee4.tar.gz |
tests: Add ServiceProxy test
Diffstat (limited to 'tests')
-rw-r--r-- | tests/meson.build | 2 | ||||
-rw-r--r-- | tests/test-service-proxy.c | 598 |
2 files changed, 599 insertions, 1 deletions
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 (); +} |