File indexing completed on 2024-12-15 05:06:15

0001 #!/usr/bin/env python3
0002 
0003 # SPDX-FileCopyrightText: 2023 Fushan Wen <qydwhotmail@gmail.com>
0004 # SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 # pylint: disable=line-too-long
0007 
0008 # For FreeBSD CI which only has python 3.9
0009 from __future__ import annotations
0010 
0011 import json
0012 import os
0013 import subprocess
0014 import sys
0015 import time
0016 import unittest
0017 from typing import Final
0018 
0019 assert "appiumtests" in os.getcwd(), "Make sure the current directory is appiumtests"
0020 sys.path.append(os.getcwd())  # for appiumtests.utils
0021 
0022 from gi.repository import Gio, GLib
0023 from utils.GLibMainLoopThread import GLibMainLoopThread
0024 from utils.mediaplayer import (Mpris2, read_base_properties, read_player_metadata, read_player_properties)
0025 
0026 KDE_VERSION: Final = 6
0027 SERVICE_NAME: Final = "org.freedesktop.DBus"
0028 ROOT_OBJECT_PATH: Final = "/"
0029 INTERFACE_NAME: Final = SERVICE_NAME
0030 KGLOBALACCELD_PATH: str = ""
0031 
0032 
0033 def name_has_owner(session_bus: Gio.DBusConnection, name: str) -> bool:
0034     """
0035     Whether the given name is available on session bus
0036     """
0037     message: Gio.DBusMessage = Gio.DBusMessage.new_method_call(SERVICE_NAME, ROOT_OBJECT_PATH, INTERFACE_NAME, "NameHasOwner")
0038     message.set_body(GLib.Variant("(s)", [name]))
0039     reply, _ = session_bus.send_message_with_reply_sync(message, Gio.DBusSendMessageFlags.NONE, 1000)
0040     return reply and reply.get_signature() == 'b' and reply.get_body().get_child_value(0).get_boolean()
0041 
0042 
0043 class MediaKeysTest(unittest.TestCase):
0044     """
0045     Tests for the media global shortcuts
0046 
0047     @see https://bugs.kde.org/show_bug.cgi?id=474531
0048     """
0049 
0050     loop_thread: GLibMainLoopThread
0051     mpris_interface: Mpris2
0052     kded: subprocess.Popen | None = None
0053     kglobalacceld: subprocess.Popen | None = None
0054 
0055     @classmethod
0056     def setUpClass(cls) -> None:
0057         cls.loop_thread = GLibMainLoopThread()
0058         cls.loop_thread.start()
0059 
0060         # Doc: https://lazka.github.io/pgi-docs/Gio-2.0/classes/DBusConnection.html
0061         session_bus: Gio.DBusConnection = Gio.bus_get_sync(Gio.BusType.SESSION)
0062 
0063         # Start KGlobalAccel daemon service
0064         if not name_has_owner(session_bus, "org.kde.kglobalaccel"):
0065             cls.kglobalacceld = subprocess.Popen([KGLOBALACCELD_PATH])
0066 
0067         if not name_has_owner(session_bus, f"org.kde.kded{KDE_VERSION}"):
0068             cls.kded = subprocess.Popen([f"kded{KDE_VERSION}"])
0069             kded_started: bool = False
0070             for _ in range(10):
0071                 if name_has_owner(session_bus, f"org.kde.kded{KDE_VERSION}"):
0072                     kded_started = True
0073                     break
0074                 print(f"waiting for kded{KDE_VERSION} to appear on the dbus session")
0075                 time.sleep(1)
0076             cls.assertTrue(kded_started, "kded is not started")
0077 
0078         cls.assertTrue(name_has_owner(session_bus, "org.kde.kglobalaccel"), "kglobalacceld is not started")
0079 
0080         kded_reply: GLib.Variant = session_bus.call_sync(f"org.kde.kded{KDE_VERSION}", "/kded", f"org.kde.kded{KDE_VERSION}", "loadModule", GLib.Variant("(s)", ["mprisservice"]), GLib.VariantType("(b)"), Gio.DBusSendMessageFlags.NONE, 1000)
0081         cls.assertTrue(kded_reply.get_child_value(0).get_boolean(), "mprisservice module is not loaded")
0082 
0083         json_path: str = os.path.join(os.getcwd(), "resources/player_a.json")
0084         with open(json_path, "r", encoding="utf-8") as f:
0085             json_dict: dict[str, list | dict] = json.load(f)
0086         metadata: list[dict[str, GLib.Variant]] = read_player_metadata(json_dict)
0087         base_properties: dict[str, GLib.Variant] = read_base_properties(json_dict)
0088         current_index: int = 1
0089         player_properties: dict[str, GLib.Variant] = read_player_properties(json_dict, metadata[current_index])
0090         cls.mpris_interface = Mpris2(metadata, base_properties, player_properties, current_index)
0091         cls.mpris_interface.registered_event.wait(timeout=10)
0092         time.sleep(1)  # Make sure kded receives the player
0093 
0094     @classmethod
0095     def tearDownClass(cls) -> None:
0096         cls.mpris_interface.quit()
0097         cls.loop_thread.quit()
0098         if cls.kded:
0099             cls.kded.terminate()
0100         if cls.kglobalacceld:
0101             cls.kglobalacceld.terminate()
0102 
0103     def setUp(self) -> None:
0104         pass
0105 
0106     def tearDown(self) -> None:
0107         pass
0108 
0109     def test_1_playpause(self) -> None:
0110         """
0111         Global shortcut for "Play/Pause media playback"
0112         """
0113         self.assertEqual(self.mpris_interface.player_properties["PlaybackStatus"].get_string(), "Stopped")
0114         subprocess.check_call(["xdotool", "key", "XF86AudioPlay"])
0115         self.assertTrue(self.mpris_interface.playback_status_set_event.wait(timeout=10))
0116         self.mpris_interface.playback_status_set_event.clear()
0117         self.assertEqual(self.mpris_interface.player_properties["PlaybackStatus"].get_string(), "Playing")
0118         subprocess.check_call(["xdotool", "key", "XF86AudioPlay"])
0119         self.assertTrue(self.mpris_interface.playback_status_set_event.wait(timeout=10))
0120         self.mpris_interface.playback_status_set_event.clear()
0121         self.assertEqual(self.mpris_interface.player_properties["PlaybackStatus"].get_string(), "Paused")
0122 
0123     def test_2_next(self) -> None:
0124         """
0125         Global shortcut for "Media playback next"
0126         """
0127         subprocess.check_call(["xdotool", "key", "XF86AudioNext"])
0128         self.assertTrue(self.mpris_interface.metadata_updated_event.wait(10))
0129         self.mpris_interface.metadata_updated_event.clear()
0130         self.assertEqual(self.mpris_interface.player_properties["Metadata"]["xesam:title"], "Konqi's Favorite")
0131 
0132         # Press again to test canNext
0133         subprocess.check_call(["xdotool", "key", "XF86AudioNext"])
0134         time.sleep(0.5)
0135         self.assertEqual(self.mpris_interface.player_properties["Metadata"]["xesam:title"], "Konqi's Favorite")
0136 
0137     def test_3_previous(self) -> None:
0138         """
0139         Global shortcut for "Media playback previous"
0140         """
0141         subprocess.check_call(["xdotool", "key", "XF86AudioPrev"])
0142         self.assertTrue(self.mpris_interface.metadata_updated_event.wait(10))
0143         self.mpris_interface.metadata_updated_event.clear()
0144         self.assertEqual(self.mpris_interface.player_properties["Metadata"]["xesam:title"], "Konqi ❤️️ Katie")
0145 
0146         subprocess.check_call(["xdotool", "key", "XF86AudioPrev"])
0147         self.assertTrue(self.mpris_interface.metadata_updated_event.wait(10))
0148         self.mpris_interface.metadata_updated_event.clear()
0149         self.assertEqual(self.mpris_interface.player_properties["Metadata"]["xesam:title"], "Katie's Favorite")
0150 
0151         # Press again to test canPrevious
0152         subprocess.check_call(["xdotool", "key", "XF86AudioPrev"])
0153         time.sleep(0.5)
0154         self.assertEqual(self.mpris_interface.player_properties["Metadata"]["xesam:title"], "Katie's Favorite")
0155 
0156     def test_4_unload_mprisservice(self) -> None:
0157         """
0158         Unload mprisservice to make sure the default keyboard shortcuts don't take effect
0159         """
0160         if self.kded is None and "KDECI_BUILD" not in os.environ:
0161             self.skipTest(f"kded{KDE_VERSION} is not run by this test")
0162 
0163         session_bus: Gio.DBusConnection = Gio.bus_get_sync(Gio.BusType.SESSION)
0164         kded_reply: GLib.Variant = session_bus.call_sync(f"org.kde.kded{KDE_VERSION}", "/kded", f"org.kde.kded{KDE_VERSION}", "unloadModule", GLib.Variant("(s)", ["mprisservice"]), GLib.VariantType("(b)"), Gio.DBusSendMessageFlags.NONE, 1000)
0165         self.assertTrue(kded_reply.get_child_value(0).get_boolean(), "mprisservice module is not loaded")
0166 
0167         last_playback_status: str = self.mpris_interface.player_properties["PlaybackStatus"].get_string()
0168         subprocess.check_call(["xdotool", "key", "XF86AudioPlay"])
0169         time.sleep(0.5)
0170         self.assertEqual(self.mpris_interface.player_properties["PlaybackStatus"].get_string(), last_playback_status)
0171 
0172         last_xesam_title: str = self.mpris_interface.player_properties["Metadata"]["xesam:title"]
0173         subprocess.check_call(["xdotool", "key", "XF86AudioNext"])
0174         time.sleep(0.5)
0175         self.assertEqual(self.mpris_interface.player_properties["Metadata"]["xesam:title"], last_xesam_title)
0176 
0177 
0178 if __name__ == '__main__':
0179     assert len(sys.argv) >= 2, "kglobalacceld is not provided"
0180     KGLOBALACCELD_PATH = sys.argv.pop()
0181     unittest.main()