#!/usr/bin/python3 # gnome-bluetooth integration test suite # # Copyright: (C) 2021 Bastien Nocera # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. import os import sys import dbus import inspect import tempfile import random import subprocess import unittest import time try: import gi from gi.repository import GLib from gi.repository import Gio gi.require_version('Gtk', '4.0') from gi.repository import Gtk except ImportError as e: sys.stderr.write('Skipping tests, PyGobject not available for Python 3, or missing GI typelibs: %s\n' % str(e)) sys.exit(77) try: gi.require_version('GIRepository', '2.0') from gi.repository import GIRepository builddir = os.getenv('top_builddir', '.') GIRepository.Repository.prepend_library_path(builddir + '/lib/') GIRepository.Repository.prepend_search_path(builddir + '/lib/') gi.require_version('GnomeBluetoothPriv', '2.0') from gi.repository import GnomeBluetoothPriv except ImportError as e: sys.stderr.write('Could not find GnomeBluetoothPriv gobject-introspection data in the build dir: %s\n' % str(e)) sys.exit(1) try: import dbusmock except ImportError: sys.stderr.write('Skipping tests, python-dbusmock not available (http://pypi.python.org/pypi/python-dbusmock).\n') sys.exit(77) # Out-of-process tests class OopTests(dbusmock.DBusTestCase): @classmethod def setUp(self): self.client = GnomeBluetoothPriv.Client.new() # used in test_pairing self.paired = False def print_tree(self, model): def print_row(model, treepath, treeiter): print("\t" * (treepath.get_depth() - 1), model[treeiter][:], sep="") model.foreach(print_row) def print_list_store(self, model): for device in model: print(f"{device.props.name}: {device.props.address}") def wait_for_mainloop(self): ml = GLib.MainLoop() GLib.timeout_add_seconds(1, ml.quit) ml.run() def wait_for_condition(self, condition): ctx = GLib.main_context_default() while not condition(): ctx.iteration(True) def test_no_adapters(self): self.wait_for_mainloop() self.assertEqual(self.client.props.num_adapters, 0) def test_one_adapter(self): self.wait_for_condition(lambda: self.client.props.num_adapters != 0) self.assertEqual(self.client.props.num_adapters, 1) def test_one_device(self): self.wait_for_condition(lambda: self.client.props.num_adapters != 0) # GListStore list_store = self.client.get_devices() self.assertEqual(list_store.get_n_items(), 1) device = list_store.get_item(0) self.assertIsNotNone(device) self.assertEqual(device.props.address, '22:33:44:55:66:77') def test_device_notify(self): bus = dbus.SystemBus() dbusmock_bluez = dbus.Interface(bus.get_object('org.bluez', '/org/bluez/hci0/dev_22_33_44_55_66_77'), 'org.freedesktop.DBus.Mock') list_store = self.client.get_devices() self.wait_for_mainloop() self.assertEqual(list_store.get_n_items(), 1) device = list_store.get_item(0) self.assertIsNotNone(device) self.assertEqual(device.props.connected, False) received_notification = False def device_notify_cb(device, pspec): nonlocal received_notification received_notification = True self.assertEqual(pspec.name, 'connected') device.connect('notify', device_notify_cb) dbusmock_bluez.UpdateProperties('org.bluez.Device1', { 'Connected': True, }) self.wait_for_condition(lambda: received_notification == True) self.assertEqual(device.props.connected, True) def test_device_removal(self): bus = dbus.SystemBus() dbusmock_bluez = dbus.Interface(bus.get_object('org.bluez', '/'), 'org.bluez.Mock') hci0_bluez = dbus.Interface(bus.get_object('org.bluez', '/org/bluez/hci0'), 'org.bluez.Adapter1') list_store = self.client.get_devices() num_devices = 0 num_mice = 0 num_devices_signal = 0 def device_added_cb(client, device): nonlocal num_devices_signal num_devices_signal += 1 def device_removed_cb(client, path): nonlocal num_devices_signal num_devices_signal -= 1 self.client.connect('device-added', device_added_cb) self.client.connect('device-removed', device_removed_cb) self.wait_for_mainloop() for i in range(1, 3): to_add = random.randrange(5, 10) to_remove = random.randrange(1, to_add) total = num_devices + to_add - to_remove print(f"Device removal iteration {i}: +{to_add} -{to_remove} = {total}") for i in range(1, to_add + 1): print(f"Adding mouse {i}") num_mice += 1 address_start = num_mice address = f"{address_start:02d}:{address_start+1:02d}:{address_start+2:02d}:" + \ f"{address_start+3:02d}:{address_start+4:02d}:{address_start+5:02d}" dbusmock_bluez.AddDevice('hci0', address, f'My Mouse {num_mice}') self.wait_for_mainloop() # self.wait_for_condition(lambda: list_store.get_n_items() == num_devices + to_add) self.assertEqual(list_store.get_n_items(), num_devices + to_add) self.assertEqual(list_store.get_n_items(), num_devices_signal) for i in range(to_remove - 1, -1, -1): print(f"Removing mouse {i}") device = list_store.get_item(i) self.assertIsNotNone(device, f"Device at index {i} in list store did not exist") hci0_bluez.RemoveDevice(device.get_object_path()) self.wait_for_mainloop() # self.wait_for_condition(lambda: list_store.get_n_items() == total) self.assertEqual(list_store.get_n_items(), total) self.assertEqual(list_store.get_n_items(), num_devices_signal) num_devices = total print(f"Device removal finishing: -{num_devices}") for i in range(num_devices - 1, -1, -1): print(f"Removing mouse {i}") device = list_store.get_item(i) hci0_bluez.RemoveDevice(device.get_object_path()) self.wait_for_mainloop() # self.wait_for_condition(lambda: list_store.get_n_items() == 0) self.assertEqual(list_store.get_n_items(), 0) self.assertEqual(num_devices_signal, 0) def test_default_adapter(self): bus = dbus.SystemBus() bluez_server = bus.get_object('org.bluez', '/org/bluez') dbusmock_bluez = dbus.Interface(bluez_server, 'org.bluez.Mock') self.wait_for_condition(lambda: self.client.props.num_adapters != 0) self.assertEqual(self.client.props.num_adapters, 2) # GListModel list_store = self.client.get_devices() self.assertEqual(list_store.get_n_items(), 1) device = list_store.get_item(0) self.assertIsNotNone(device) self.assertEqual(device.props.address, '22:33:44:55:66:77') # Set default adapter in setup mode self.assertEqual (self.client.props.default_adapter_setup_mode, False) default_adapter_path = self.client.props.default_adapter self.client.props.default_adapter_setup_mode = True self.wait_for_mainloop() self.assertEqual(self.client.props.default_adapter_setup_mode, True) # Remove default adapter dbusmock_bluez.RemoveAdapter('hci1') self.wait_for_condition(lambda: self.client.props.num_adapters != 2) self.assertEqual(self.client.props.num_adapters, 1) self.assertNotEqual(self.client.props.default_adapter, default_adapter_path) self.assertEqual(self.client.props.default_adapter_setup_mode, False) # GListModel self.assertEqual(list_store.get_n_items(), 1) device = list_store.get_item(0) self.assertIsNotNone(device) self.assertEqual(device.props.address, '11:22:33:44:55:66') # Re-add the old adapter, device is still there dbusmock_bluez.AddAdapter('hci1', 'my-computer #2') # GListModel self.assertEqual(list_store.get_n_items(), 1) device = list_store.get_item(0) self.assertIsNotNone(device) self.assertEqual(device.props.address, '11:22:33:44:55:66') def _pair_cb(self, client, result, user_data=None): success, path = client.setup_device_finish(result) self.assertEqual(success, True) address = '11:22:33:44:55:66' self.assertEqual(path, '/org/bluez/hci0/dev_' + address.replace(':', '_')) self.paired = True def test_pairing(self): self.wait_for_condition(lambda: self.client.props.num_adapters != 0) self.assertEqual(self.client.props.num_adapters, 1) model = self.client.get_devices() # Get first device device = model.get_item(0) self.assertEqual(device.props.address, '11:22:33:44:55:66') self.assertEqual(device.props.paired, False) self.client.setup_device (device.get_object_path(), True, None, self._pair_cb) self.wait_for_condition(lambda: self.paired == True) self.assertEqual(self.paired, True) self.assertEqual(device.props.paired, True) self.assertEqual(device.props.icon, 'phone') def test_agent(self): agent = GnomeBluetoothPriv.Agent.new ('/org/gnome/bluetooth/integration_test') self.assertIsNotNone(agent) # Process D-Bus daemon appearing and agent being registered self.wait_for_mainloop() self.assertTrue(agent.register()) self.assertTrue(agent.unregister()) class Tests(dbusmock.DBusTestCase): @classmethod def setUpClass(cls): os.environ['G_MESSAGES_DEBUG'] = 'all' os.environ['G_DEBUG'] = 'fatal_warnings' cls.start_system_bus() cls.dbus_con = cls.get_dbus(True) (cls.p_mock, cls.obj_bluez) = cls.spawn_server_template( 'bluez5', {}) cls.exec_path = [sys.argv[0]] if os.getenv('VALGRIND') != None: cls.exec_path = ['valgrind'] + cls.exec_path @classmethod def tearDownClass(cls): cls.p_mock.terminate() cls.p_mock.wait() def setUp(self): self.obj_bluez.Reset() self.dbusmock = dbus.Interface(self.obj_bluez, dbusmock.MOCK_IFACE) self.dbusmock_bluez = dbus.Interface(self.obj_bluez, 'org.bluez.Mock') def run_test_process(self): # Get the calling function's name test_name = inspect.stack()[1][3] # And run the test with the same name in the OopTests class in a separate process out = subprocess.run(self.exec_path + ['OopTests.' + test_name], capture_output=True) self.assertEqual(out.returncode, 0, "Running test " + test_name + " failed:" + out.stderr.decode('UTF-8') + '\n\n\nSTDOUT:\n' + out.stdout.decode('UTF-8')) if os.getenv('VALGRIND') != None: print(out.stderr.decode('UTF-8')) def test_no_adapters(self): self.run_test_process() def test_one_adapter(self): self.dbusmock_bluez.AddAdapter('hci0', 'my-computer') self.run_test_process() def test_one_device(self): self.dbusmock_bluez.AddAdapter('hci0', 'my-computer') self.dbusmock_bluez.AddDevice('hci0', '22:33:44:55:66:77', 'My Mouse') self.run_test_process() def test_device_notify(self): self.dbusmock_bluez.AddAdapter('hci0', 'my-computer') self.dbusmock_bluez.AddDevice('hci0', '22:33:44:55:66:77', 'My Mouse') self.run_test_process() def test_device_removal(self): self.dbusmock_bluez.AddAdapter('hci0', 'my-computer') self.run_test_process() def test_default_adapter(self): self.dbusmock_bluez.AddAdapter('hci0', 'my-computer') self.dbusmock_bluez.AddAdapter('hci1', 'my-computer #2') self.dbusmock_bluez.AddDevice('hci0', '11:22:33:44:55:66', 'My Phone') self.dbusmock_bluez.AddDevice('hci1', '22:33:44:55:66:77', 'My Mouse') self.run_test_process() def test_pairing(self): adapter_name = 'hci0' self.dbusmock_bluez.AddAdapter(adapter_name, 'my-computer') address = '11:22:33:44:55:66' alias = 'My Phone' path = self.dbusmock_bluez.AddDevice(adapter_name, address, alias) self.assertEqual(path, '/org/bluez/' + adapter_name + '/dev_' + address.replace(':', '_')) self.run_test_process() def test_agent(self): self.run_test_process() if __name__ == '__main__': unittest.main()