libportal/0.6-backports.patch

1006 lines
33 KiB
Diff
Raw Normal View History

2023-01-18 09:26:52 +00:00
From 6a52f680cf4ceda9feb8724793c090cd2258f6f7 Mon Sep 17 00:00:00 2001
From: Billy <billyaraujo@gmail.com>
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 <peter.hutterer@who-t.net>
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 <peter.hutterer@who-t.net>
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 <peter.hutterer@who-t.net>
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 <peter.hutterer@who-t.net>
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 <georges.stavracas@gmail.com>
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 <peter.hutterer@who-t.net>
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