From 6a52f680cf4ceda9feb8724793c090cd2258f6f7 Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 24 May 2022 17:45:59 +0100 Subject: [PATCH 1/7] Fixed issue where y was used instead of h. --- portal-test/gtk3/portal-test-win.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal-test/gtk3/portal-test-win.c b/portal-test/gtk3/portal-test-win.c index 9d50708..e2432c6 100644 --- a/portal-test/gtk3/portal-test-win.c +++ b/portal-test/gtk3/portal-test-win.c @@ -594,7 +594,7 @@ session_started (GObject *source, g_variant_lookup (props, "size", "(ii)", &w, &h); if (s->len > 0) g_string_append (s, "\n"); - g_string_append_printf (s, "Stream %d: %dx%d @ %d,%d", id, w, y, x, y); + g_string_append_printf (s, "Stream %d: %dx%d @ %d,%d", id, w, h, x, y); g_variant_unref (props); } -- 2.39.0 From a22753772a28e225e4e91b65add10c23ad106243 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Fri, 24 Jun 2022 12:58:32 +1000 Subject: [PATCH 2/7] remote: call the right DBus method for TouchUp --- libportal/remote.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libportal/remote.c b/libportal/remote.c index e7fb115..ebdffe0 100644 --- a/libportal/remote.c +++ b/libportal/remote.c @@ -1160,7 +1160,7 @@ xdp_session_touch_up (XdpSession *session, PORTAL_BUS_NAME, PORTAL_OBJECT_PATH, "org.freedesktop.portal.RemoteDesktop", - "NotifyTouchMotion", + "NotifyTouchUp", g_variant_new ("(oa{sv}u)", session->id, &options, slot), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); } -- 2.39.0 From 6e25d5cb28412e6a4df553e9f798200b19f1c410 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Thu, 30 Jun 2022 14:00:39 +1000 Subject: [PATCH 3/7] spawn: initialize the option builder ../libportal/spawn.c:176:60: warning: variable 'opt_builder' is uninitialized when used here [-Wuninitialized] opt_builder), --- libportal/spawn.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libportal/spawn.c b/libportal/spawn.c index 20ef005..81a03af 100644 --- a/libportal/spawn.c +++ b/libportal/spawn.c @@ -131,6 +131,8 @@ do_spawn (SpawnCall *call) ensure_spawn_exited_connection (call->portal); + g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_init (&fds_builder, G_VARIANT_TYPE ("a{uh}")); if (call->n_fds > 0) { -- 2.39.0 From 030a6164a94c6c173caabcf5a3377189be951474 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Thu, 30 Jun 2022 14:06:32 +1000 Subject: [PATCH 4/7] portal: fix the strcmps on the cgroup hierarchies Fixes ../libportal/portal.c:344:12: warning: logical not is only applied to the left hand side of this comparison [-Wlogical-not-parentheses] !strcmp (controller, ":") != 0) && --- libportal/portal.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libportal/portal.c b/libportal/portal.c index 5e72089..32a34d7 100644 --- a/libportal/portal.c +++ b/libportal/portal.c @@ -304,9 +304,10 @@ _xdp_parse_cgroup_file (FILE *f, gboolean *is_snap) /* Only consider the freezer, systemd group or unified cgroup * hierarchies */ - if ((!strcmp (controller, "freezer:") != 0 || - !strcmp (controller, "name=systemd:") != 0 || - !strcmp (controller, ":") != 0) && + if (controller != NULL && + (g_str_equal (controller, "freezer:") || + g_str_equal (controller, "name=systemd:") || + g_str_equal (controller, ":")) && strstr (cgroup, "/snap.") != NULL) { *is_snap = TRUE; -- 2.39.0 From 953dd354211d70482d9efc54654176ed6bf3bf4e Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Wed, 29 Jun 2022 15:10:35 +1000 Subject: [PATCH 5/7] session: replace g_free with g_clear_pointer --- libportal/session.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libportal/session.c b/libportal/session.c index b505d0b..0b1f02a 100644 --- a/libportal/session.c +++ b/libportal/session.c @@ -55,8 +55,8 @@ xdp_session_finalize (GObject *object) g_dbus_connection_signal_unsubscribe (session->portal->bus, session->signal_id); g_clear_object (&session->portal); - g_free (session->restore_token); - g_free (session->id); + g_clear_pointer (&session->restore_token, g_free); + g_clear_pointer (&session->id, g_free); g_clear_pointer (&session->streams, g_variant_unref); G_OBJECT_CLASS (xdp_session_parent_class)->finalize (object); -- 2.39.0 From f56281857dce8e6515fab6030406112a251ff1e7 Mon Sep 17 00:00:00 2001 From: Georges Basile Stavracas Neto Date: Wed, 12 Oct 2022 13:15:18 -0300 Subject: [PATCH 6/7] background: Add background status Add the correspondent background status API. See https://github.com/flatpak/xdg-desktop-portal/pull/901 --- libportal/background.c | 163 +++++++++++++++++++++++++++++++++++++ libportal/background.h | 11 +++ libportal/portal-private.h | 3 + 3 files changed, 177 insertions(+) diff --git a/libportal/background.c b/libportal/background.c index d6c8348..f47570f 100644 --- a/libportal/background.c +++ b/libportal/background.c @@ -20,9 +20,116 @@ #include "config.h" +#include "session-private.h" #include "background.h" #include "portal-private.h" +typedef struct { + XdpPortal *portal; + GTask *task; + char *status_message; +} SetStatusCall; + +static void +set_status_call_free (SetStatusCall *call) +{ + g_clear_pointer (&call->status_message, g_free); + g_clear_object (&call->portal); + g_clear_object (&call->task); + g_free (call); +} + +static void +set_status_returned (GObject *object, + GAsyncResult *result, + gpointer data) +{ + SetStatusCall *call = data; + GError *error = NULL; + g_autoptr(GVariant) ret = NULL; + + ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + if (error) + g_task_return_error (call->task, error); + else + g_task_return_boolean (call->task, TRUE); + + set_status_call_free (call); +} + +static void +set_status (SetStatusCall *call) +{ + GVariantBuilder options; + + g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT); + + if (call->status_message) + g_variant_builder_add (&options, "{sv}", "message", g_variant_new_string (call->status_message)); + + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.portal.Background", + "SetStatus", + g_variant_new ("(a{sv})", &options), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + g_task_get_cancellable (call->task), + set_status_returned, + call); +} + +static void +get_background_version_returned (GObject *object, + GAsyncResult *result, + gpointer data) +{ + g_autoptr(GVariant) version_variant = NULL; + g_autoptr(GVariant) ret = NULL; + SetStatusCall *call = data; + GError *error = NULL; + + ret = g_dbus_connection_call_finish (G_DBUS_CONNECTION (object), result, &error); + if (error) + { + g_task_return_error (call->task, error); + set_status_call_free (call); + return; + } + + g_variant_get_child (ret, 0, "v", &version_variant); + call->portal->background_interface_version = g_variant_get_uint32 (version_variant); + + if (call->portal->background_interface_version < 2) + { + g_task_return_new_error (call->task, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, + "Background portal does not implement version 2 of the interface"); + set_status_call_free (call); + return; + } + + set_status (call); +} + +static void +get_background_interface_version (SetStatusCall *call) +{ + g_dbus_connection_call (call->portal->bus, + PORTAL_BUS_NAME, + PORTAL_OBJECT_PATH, + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new ("(ss)", "org.freedesktop.portal.Background", "version"), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + g_task_get_cancellable (call->task), + get_background_version_returned, + call); +} + typedef struct { XdpPortal *portal; XdpParent *parent; @@ -282,3 +389,59 @@ xdp_portal_request_background_finish (XdpPortal *portal, return g_task_propagate_boolean (G_TASK (result), error); } + +/** + * xdp_portal_set_background_status: + * @portal: a [class@Portal] + * @status_message: (nullable): status message when running in background + * @cancellable: (nullable): optional [class@Gio.Cancellable] + * @callback: (scope async): a callback to call when the request is done + * @data: (closure): data to pass to @callback + * + * Sets the status information of the application, for when it's running + * in background. + */ +void +xdp_portal_set_background_status (XdpPortal *portal, + const char *status_message, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + SetStatusCall *call; + + g_return_if_fail (XDP_IS_PORTAL (portal)); + + call = g_new0 (SetStatusCall, 1); + call->portal = g_object_ref (portal); + call->status_message = g_strdup (status_message); + call->task = g_task_new (portal, cancellable, callback, data); + g_task_set_source_tag (call->task, xdp_portal_set_background_status); + + if (portal->background_interface_version == 0) + get_background_interface_version (call); + else + set_status (call); +} + +/** + * xdp_portal_set_background_status_finish: + * @portal: a [class@Portal] + * @result: a [iface@Gio.AsyncResult] + * @error: return location for an error + * + * Finishes setting the background status of the application. + * + * Returns: %TRUE if successfully set status, %FALSE otherwise + */ +gboolean +xdp_portal_set_background_status_finish (XdpPortal *portal, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (XDP_IS_PORTAL (portal), FALSE); + g_return_val_if_fail (g_task_is_valid (result, portal), FALSE); + g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == xdp_portal_set_background_status, FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/libportal/background.h b/libportal/background.h index a22090d..5ce1734 100644 --- a/libportal/background.h +++ b/libportal/background.h @@ -52,5 +52,16 @@ gboolean xdp_portal_request_background_finish (XdpPortal *portal, GAsyncResult *result, GError **error); +XDP_PUBLIC +void xdp_portal_set_background_status (XdpPortal *portal, + const char *status_message, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data); + +XDP_PUBLIC +gboolean xdp_portal_set_background_status_finish (XdpPortal *portal, + GAsyncResult *result, + GError **error); G_END_DECLS diff --git a/libportal/portal-private.h b/libportal/portal-private.h index 6728055..542e1bb 100644 --- a/libportal/portal-private.h +++ b/libportal/portal-private.h @@ -51,6 +51,9 @@ struct _XdpPortal { /* screencast */ guint screencast_interface_version; + + /* background */ + guint background_interface_version; }; #define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop" -- 2.39.0 From 631a16363236fba681ad848166619e14f0cf5637 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Thu, 26 May 2022 12:49:50 +1000 Subject: [PATCH 7/7] test: add a pytest/dbusmock-based test suite Using python and dbusmock makes it trivial to add a large number of tests for libportal only, without requiring an actual portal implementation for the Portal interface to be tested. Included here is the wallpaper portal as an example, hooked into meson test. A helper script is provided too for those lacking meson devenv, $ ./test/gir-testenv.sh $ cd test $ pytest --verbose --log-level=DEBUG [... other pytest arguments ...] The test setup uses dbusmock interface templates (see pyportaltest/templates) to handle the actual DBus calls. Because DBus uses a singleton for the session bus, we need libportal to specifically connect to the address given in the environment - otherwise starting mock dbus services has no effect. This test suite depends on dbusmock commit 4a191d8ba293: "mockobject: allow sending signals with extra details" from https://github.com/martinpitt/python-dbusmock/pull/129 Without this, the EmitSignalDetailed() method does not exist/work, but without this method we cannot receive signals. --- .github/workflows/build.yml | 6 +- libportal/portal.c | 37 +++++- tests/gir-testenv.sh | 31 +++++ tests/meson.build | 19 +++ tests/pyportaltest/__init__.py | 149 ++++++++++++++++++++++ tests/pyportaltest/templates/__init__.py | 94 ++++++++++++++ tests/pyportaltest/templates/wallpaper.py | 48 +++++++ tests/pyportaltest/test_wallpaper.py | 117 +++++++++++++++++ 8 files changed, 497 insertions(+), 4 deletions(-) create mode 100755 tests/gir-testenv.sh create mode 100644 tests/pyportaltest/__init__.py create mode 100644 tests/pyportaltest/templates/__init__.py create mode 100644 tests/pyportaltest/templates/wallpaper.py create mode 100644 tests/pyportaltest/test_wallpaper.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66d9fb4..133a998 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y libglib2.0 gettext dbus meson libgirepository1.0-dev libgtk-3-dev valac + sudo apt-get install -y libglib2.0 gettext dbus meson libgirepository1.0-dev libgtk-3-dev valac python3-pytest python3-dbusmock - name: Check out libportal uses: actions/checkout@v1 - name: Configure libportal @@ -55,7 +55,7 @@ jobs: - name: Install dependencies run: | apt-get update - apt-get install -y libglib2.0 gettext dbus meson libgirepository1.0-dev libgtk-3-dev libgtk-4-dev valac python3-pip + apt-get install -y libglib2.0 gettext dbus meson libgirepository1.0-dev libgtk-3-dev libgtk-4-dev valac python3-pip python3-dbusmock pip3 install gi-docgen echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Check out libportal @@ -73,7 +73,7 @@ jobs: steps: - name: Install dependencies run: | - dnf install -y meson gcc gobject-introspection-devel gtk3-devel gtk4-devel gi-docgen vala git + dnf install -y meson gcc gobject-introspection-devel gtk3-devel gtk4-devel gi-docgen vala git python3-pytest python3-dbusmock - name: Check out libportal uses: actions/checkout@v1 - name: Configure libportal diff --git a/libportal/portal.c b/libportal/portal.c index 32a34d7..7765bc7 100644 --- a/libportal/portal.c +++ b/libportal/portal.c @@ -254,12 +254,47 @@ xdp_portal_class_init (XdpPortalClass *klass) G_TYPE_VARIANT); } +static GDBusConnection * +create_bus_from_address (const char *address, + GError **error) +{ + g_autoptr(GDBusConnection) bus = NULL; + + if (!address) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Missing D-Bus session bus address"); + return NULL; + } + + bus = g_dbus_connection_new_for_address_sync (address, + G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | + G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION, + NULL, NULL, + error); + return g_steal_pointer (&bus); +} + static void xdp_portal_init (XdpPortal *portal) { + g_autoptr(GError) error = NULL; int i; - portal->bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + /* g_bus_get_sync() returns a singleton. In the test suite we may restart + * the session bus, so we have to manually connect to the new bus */ + if (getenv ("LIBPORTAL_TEST_SUITE")) + portal->bus = create_bus_from_address (getenv ("DBUS_SESSION_BUS_ADDRESS"), &error); + else + portal->bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + + if (error) + { + g_critical ("Failed to create XdpPortal instance: %s\n", error->message); + abort (); + } + + g_assert (portal->bus != NULL); + portal->sender = g_strdup (g_dbus_connection_get_unique_name (portal->bus) + 1); for (i = 0; portal->sender[i]; i++) if (portal->sender[i] == '.') diff --git a/tests/gir-testenv.sh b/tests/gir-testenv.sh new file mode 100755 index 0000000..6cb8e47 --- /dev/null +++ b/tests/gir-testenv.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# +# Wrapper to set up the right environment variables and start a nested +# shell. Usage: +# +# $ ./tests/gir-testenv.sh +# (nested shell) $ pytest +# (nested shell) $ exit +# +# If you have meson 0.58 or later, you can instead do: +# $ meson devenv -C builddir +# (nested shell) $ cd ../tests +# (nested shell) $ pytest +# (nested shell) $ exit +# + +builddir=$(find $PWD -name meson-logs -printf "%h" -quit) + +if [ -z "$builddir" ]; then + echo "Unable to find meson builddir" + exit 1 +fi + +echo "Using meson builddir: $builddir" + +export LD_LIBRARY_PATH="$builddir/libportal:$LD_LIBRARY_PATH" +export GI_TYPELIB_PATH="$builddir/libportal:$GI_TYPELIB_PATH" + +echo "pytest must be run from within the tests/ directory" +# Don't think this is portable, but oh well +${SHELL} diff --git a/tests/meson.build b/tests/meson.build index ffc415f..0c67335 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,3 +1,22 @@ if 'qt5' in backends subdir('qt5') endif + +if meson.version().version_compare('>= 0.56.0') + pytest = find_program('pytest-3', 'pytest', required: false) + pymod = import('python') + python = pymod.find_installation('python3', modules: ['dbus', 'dbusmock'], required: false) + + if pytest.found() and python.found() + test_env = environment() + test_env.set('LD_LIBRARY_PATH', meson.project_build_root() / 'libportal') + test_env.set('GI_TYPELIB_PATH', meson.project_build_root() / 'libportal') + + test('pytest', + pytest, + args: ['--verbose', '--log-level=DEBUG'], + env: test_env, + workdir: meson.current_source_dir() + ) + endif +endif diff --git a/tests/pyportaltest/__init__.py b/tests/pyportaltest/__init__.py new file mode 100644 index 0000000..e298612 --- /dev/null +++ b/tests/pyportaltest/__init__.py @@ -0,0 +1,149 @@ +# SPDX-License-Identifier: LGPL-3.0-only +# +# This file is formatted with Python Black + +from typing import Any, Dict, List, Tuple + +import gi +from gi.repository import GLib +from dbus.mainloop.glib import DBusGMainLoop + +import dbus +import dbusmock +import fcntl +import logging +import os +import pytest +import subprocess + +logging.basicConfig(format="%(levelname)s | %(name)s: %(message)s", level=logging.DEBUG) +logger = logging.getLogger("pyportaltest") + +DBusGMainLoop(set_as_default=True) + +# Uncomment this to have dbus-monitor listen on the normal session address +# rather than the test DBus. This can be useful for cases where *something* +# messes up and tests run against the wrong bus. +# +# session_dbus_address = os.environ["DBUS_SESSION_BUS_ADDRESS"] + + +def start_dbus_monitor() -> "subprocess.Process": + import subprocess + + env = os.environ.copy() + try: + env["DBUS_SESSION_BUS_ADDRESS"] = session_dbus_address + except NameError: + # See comment above + pass + + argv = ["dbus-monitor", "--session"] + mon = subprocess.Popen(argv, env=env) + + def stop_dbus_monitor(): + mon.terminate() + mon.wait() + + GLib.timeout_add(2000, stop_dbus_monitor) + return mon + + +class PortalTest(dbusmock.DBusTestCase): + """ + Parent class for portal tests. Subclass from this and name it after the + portal, e.g. ``TestWallpaper``. + + .. attribute:: portal_interface + + The :class:`dbus.Interface` referring to our portal + + .. attribute:: properties_interface + + A convenience :class:`dbus.Interface` referring to the DBus Properties + interface, call ``Get``, ``Set`` or ``GetAll`` on this interface to + retrieve the matching property/properties. + + .. attribute:: mock_interface + + The DBusMock :class:`dbus.Interface` that controls our DBus + appearance. + + """ + @classmethod + def setUpClass(cls): + if cls.__name__ != "PortalTest": + cls.PORTAL_NAME = cls.__name__.removeprefix("Test") + cls.INTERFACE_NAME = f"org.freedesktop.portal.{cls.PORTAL_NAME}" + os.environ["LIBPORTAL_TEST_SUITE"] = "1" + + try: + dbusmock.mockobject.DBusMockObject.EmitSignalDetailed + except AttributeError: + pytest.skip("Updated version of dbusmock required") + + def setUp(self): + self.p_mock = None + self._mainloop = None + self.dbus_monitor = None + + def setup_daemon(self, params=None): + """ + Start a DBusMock daemon in a separate process + """ + self.start_session_bus() + self.p_mock, self.obj_portal = self.spawn_server_template( + template=f"pyportaltest/templates/{self.PORTAL_NAME.lower()}.py", + parameters=params, + stdout=subprocess.PIPE, + ) + flags = fcntl.fcntl(self.p_mock.stdout, fcntl.F_GETFL) + fcntl.fcntl(self.p_mock.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK) + self.mock_interface = dbus.Interface(self.obj_portal, dbusmock.MOCK_IFACE) + self.properties_interface = dbus.Interface( + self.obj_portal, dbus.PROPERTIES_IFACE + ) + self.portal_interface = dbus.Interface(self.obj_portal, self.INTERFACE_NAME) + + self.dbus_monitor = start_dbus_monitor() + + def tearDown(self): + if self.p_mock: + if self.p_mock.stdout: + out = (self.p_mock.stdout.read() or b"").decode("utf-8") + if out: + print(out) + self.p_mock.stdout.close() + self.p_mock.terminate() + self.p_mock.wait() + + if self.dbus_monitor: + self.dbus_monitor.terminate() + self.dbus_monitor.wait() + + @property + def mainloop(self): + """ + The mainloop for this test. This mainloop automatically quits after a + fixed timeout, but only on the first run. That's usually enough for + tests, if you need to call mainloop.run() repeatedly ensure that a + timeout handler is set to ensure quick test case failure in case of + error. + """ + if self._mainloop is None: + + def quit(): + self._mainloop.quit() + self._mainloop = None + + self._mainloop = GLib.MainLoop() + GLib.timeout_add(2000, quit) + + return self._mainloop + + def assert_version_eq(self, version: int): + """Assert the given version number is the one our portal exports""" + interface_name = self.INTERFACE_NAME + params = {} + self.setup_daemon(params) + assert self.properties_interface.Get(interface_name, "version") == version diff --git a/tests/pyportaltest/templates/__init__.py b/tests/pyportaltest/templates/__init__.py new file mode 100644 index 0000000..c94a5cd --- /dev/null +++ b/tests/pyportaltest/templates/__init__.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: LGPL-3.0-only +# +# This file is formatted with Python Black + +from dbusmock import DBusMockObject +from typing import Dict, Any, NamedTuple, Optional +from itertools import count +from gi.repository import GLib + +import dbus +import logging + + +ASVType = Dict[str, Any] + +logging.basicConfig(format="%(levelname).1s|%(name)s: %(message)s", level=logging.DEBUG) +logger = logging.getLogger("templates") + + +class Response(NamedTuple): + response: int + results: ASVType + + +class Request: + _token_counter = count() + + def __init__( + self, bus_name: dbus.service.BusName, sender: str, options: Optional[ASVType] + ): + options = options or {} + sender_token = sender.removeprefix(":").replace(".", "_") + handle_token = options.get("handle_token", next(self._token_counter)) + self.sender = sender + self.handle = ( + f"/org/freedesktop/portal/desktop/request/{sender_token}/{handle_token}" + ) + self.mock = DBusMockObject( + bus_name=bus_name, + path=self.handle, + interface="org.freedesktop.portal.Request", + props={}, + ) + self.mock.AddMethod("", "Close", "", "", "self.RemoveObject(self.path)") + + def respond(self, response: Response, delay: int = 0): + def respond(): + logger.debug(f"Request.Response on {self.handle}: {response}") + self.mock.EmitSignalDetailed( + "", + "Response", + "ua{sv}", + [dbus.UInt32(response.response), response.results], + details={"destination": self.sender}, + ) + + if delay > 0: + GLib.timeout_add(delay, respond) + else: + respond() + + +class Session: + _token_counter = count() + + def __init__( + self, bus_name: dbus.service.BusName, sender: str, options: Optional[ASVType] + ): + options = options or {} + sender_token = sender.removeprefix(":").replace(".", "_") + handle_token = options.get("session_handle_token", next(self._token_counter)) + self.sender = sender + self.handle = ( + f"/org/freedesktop/portal/desktop/session/{sender_token}/{handle_token}" + ) + self.mock = DBusMockObject( + bus_name=bus_name, + path=self.handle, + interface="org.freedesktop.portal.Session", + props={}, + ) + self.mock.AddMethod("", "Close", "", "", "self.RemoveObject(self.path)") + + def close(self, details: ASVType, delay: int = 0): + def respond(): + logger.debug(f"Session.Closed on {self.handle}: {details}") + self.mock.EmitSignalDetailed( + "", "Closed", "a{sv}", [details], destination=self.sender + ) + + if delay > 0: + GLib.timeout_add(delay, respond) + else: + respond() diff --git a/tests/pyportaltest/templates/wallpaper.py b/tests/pyportaltest/templates/wallpaper.py new file mode 100644 index 0000000..f0371b0 --- /dev/null +++ b/tests/pyportaltest/templates/wallpaper.py @@ -0,0 +1,48 @@ +# SPDX-License-Identifier: LGPL-3.0-only +# +# This file is formatted with Python Black + +from pyportaltest.templates import Request, Response, ASVType +from typing import Dict, List, Tuple, Iterator + +import dbus.service +import logging + +logger = logging.getLogger(f"templates.{__name__}") + +BUS_NAME = "org.freedesktop.portal.Desktop" +MAIN_OBJ = "/org/freedesktop/portal/desktop" +SYSTEM_BUS = False +MAIN_IFACE = "org.freedesktop.portal.Wallpaper" + + +def load(mock, parameters=None): + logger.debug(f"loading {MAIN_IFACE} template") + mock.delay = 500 + + mock.response = parameters.get("response", 0) + + mock.AddProperties( + MAIN_IFACE, + dbus.Dictionary({"version": dbus.UInt32(parameters.get("version", 1))}), + ) + + +@dbus.service.method( + MAIN_IFACE, + sender_keyword="sender", + in_signature="ssa{sv}", + out_signature="o", +) +def SetWallpaperURI(self, parent_window, uri, options, sender): + try: + logger.debug(f"SetWallpaperURI: {parent_window}, {uri}, {options}") + request = Request(bus_name=self.bus_name, sender=sender, options=options) + + response = Response(self.response, {}) + + request.respond(response, delay=self.delay) + + return request.handle + except Exception as e: + logger.critical(e) diff --git a/tests/pyportaltest/test_wallpaper.py b/tests/pyportaltest/test_wallpaper.py new file mode 100644 index 0000000..def66fc --- /dev/null +++ b/tests/pyportaltest/test_wallpaper.py @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: LGPL-3.0-only +# +# This file is formatted with Python Black + +from . import PortalTest + +import gi +import logging + +gi.require_version("Xdp", "1.0") +from gi.repository import GLib, Xdp + +logger = logging.getLogger(__name__) + + +class TestWallpaper(PortalTest): + def test_version(self): + self.assert_version_eq(1) + + def set_wallpaper( + self, uri_to_set: str, set_on: Xdp.WallpaperFlags, show_preview: bool + ): + params = {} + self.setup_daemon(params) + + xdp = Xdp.Portal.new() + assert xdp is not None + + flags = { + "background": Xdp.WallpaperFlags.BACKGROUND, + "lockscreen": Xdp.WallpaperFlags.LOCKSCREEN, + "both": Xdp.WallpaperFlags.BACKGROUND | Xdp.WallpaperFlags.LOCKSCREEN, + }[set_on] + + if show_preview: + flags |= Xdp.WallpaperFlags.PREVIEW + + wallpaper_was_set = False + + def set_wallpaper_done(portal, task, data): + nonlocal wallpaper_was_set + wallpaper_was_set = portal.set_wallpaper_finish(task) + self.mainloop.quit() + + xdp.set_wallpaper( + parent=None, + uri=uri_to_set, + flags=flags, + cancellable=None, + callback=set_wallpaper_done, + data=None, + ) + + self.mainloop.run() + + method_calls = self.mock_interface.GetMethodCalls("SetWallpaperURI") + assert len(method_calls) == 1 + timestamp, args = method_calls.pop(0) + parent, uri, options = args + assert uri == uri_to_set + assert options["set-on"] == set_on + assert options["show-preview"] == show_preview + + assert wallpaper_was_set + + def test_set_wallpaper_background(self): + self.set_wallpaper("https://background.nopreview", "background", False) + + def test_set_wallpaper_background_preview(self): + self.set_wallpaper("https://background.preview", "background", True) + + def test_set_wallpaper_lockscreen(self): + self.set_wallpaper("https://lockscreen.nopreview", "lockscreen", False) + + def test_set_wallpaper_lockscreen_preview(self): + self.set_wallpaper("https://lockscreen.preview", "lockscreen", True) + + def test_set_wallpaper_both(self): + self.set_wallpaper("https://both.nopreview", "both", False) + + def test_set_wallpaper_both_preview(self): + self.set_wallpaper("https://both.preview", "both", True) + + def test_set_wallpaper_cancel(self): + params = {"response": 1} + self.setup_daemon(params) + + xdp = Xdp.Portal.new() + assert xdp is not None + + flags = Xdp.WallpaperFlags.BACKGROUND + + wallpaper_was_set = False + + def set_wallpaper_done(portal, task, data): + nonlocal wallpaper_was_set + try: + wallpaper_was_set = portal.set_wallpaper_finish(task) + except GLib.GError: + pass + self.mainloop.quit() + + xdp.set_wallpaper( + parent=None, + uri="https://ignored.anyway", + flags=flags, + cancellable=None, + callback=set_wallpaper_done, + data=None, + ) + + self.mainloop.run() + + method_calls = self.mock_interface.GetMethodCalls("SetWallpaperURI") + assert len(method_calls) == 1 + + assert not wallpaper_was_set -- 2.39.0