diff --git a/0.6-backports.patch b/0.6-backports.patch new file mode 100644 index 0000000..fccfbc1 --- /dev/null +++ b/0.6-backports.patch @@ -0,0 +1,1005 @@ +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 + diff --git a/libportal.spec b/libportal.spec index fea548d..07a628c 100644 --- a/libportal.spec +++ b/libportal.spec @@ -1,6 +1,6 @@ Name: libportal Version: 0.6 -Release: 4%{?dist} +Release: 5%{?dist} Summary: Flatpak portal library License: LGPLv3 Url: https://github.com/flatpak/libportal @@ -11,6 +11,7 @@ BuildRequires: gcc-c++ BuildRequires: gi-docgen BuildRequires: meson BuildRequires: vala +BuildRequires: git BuildRequires: pkgconfig(gio-2.0) BuildRequires: pkgconfig(gio-unix-2.0) BuildRequires: pkgconfig(gobject-introspection-1.0) @@ -21,6 +22,9 @@ BuildRequires: pkgconfig(Qt5Gui) BuildRequires: pkgconfig(Qt5Widgets) BuildRequires: pkgconfig(Qt5X11Extras) +# Backport from post-0.6 fixes +Patch0: 0.6-backports.patch + %description libportal provides GIO-style asynchronous APIs for most Flatpak portals. @@ -104,7 +108,7 @@ libportal provides GIO-style asynchronous APIs for most Flatpak portals. This package provides development documentations for libportal. %prep -%autosetup -p1 +%autosetup -p1 -S git_am %build %meson @@ -163,6 +167,9 @@ This package provides development documentations for libportal. %{_datadir}/doc/libportal-1 %changelog +* Wed Jan 18 2023 Bastien Nocera - 0.6-5 +- Backport post-0.6 bug fixes + * Thu Dec 15 2022 Benjamin A. Beasley - 0.6-4 - Ensure correct fonts are installed for HTML docs