summaryrefslogtreecommitdiffstats
path: root/libraries/libportal/0.6-backports.patch
diff options
context:
space:
mode:
Diffstat (limited to 'libraries/libportal/0.6-backports.patch')
-rw-r--r--libraries/libportal/0.6-backports.patch1005
1 files changed, 1005 insertions, 0 deletions
diff --git a/libraries/libportal/0.6-backports.patch b/libraries/libportal/0.6-backports.patch
new file mode 100644
index 0000000000..fccfbc1c3e
--- /dev/null
+++ b/libraries/libportal/0.6-backports.patch
@@ -0,0 +1,1005 @@
+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
+