summaryrefslogtreecommitdiff
path: root/utils
diff options
context:
space:
mode:
authorSam Thursfield <sam@afuera.me.uk>2019-08-27 00:04:11 +0300
committerSam Thursfield <sam@afuera.me.uk>2019-09-14 12:09:19 +0200
commitdae63fea40d9b045bca0e39e855ee61c456e944d (patch)
treed93ddb230236e60e56618bc236879071a980dc6c /utils
parent2d9c4449ed908214268e23aa5bf7c77cc2cf8601 (diff)
downloadtracker-dae63fea40d9b045bca0e39e855ee61c456e944d.tar.gz
functional-tests: Start daemons through D-Bus autolaunch
Instead of manually running and managing Tracker daemon processes manually in the test, we now rely on our private D-Bus daemon to do so. This makes the test environment more like a real Tracker deployment. Log output from the D-Bus daemon is now captured and output through the Python logging system. This allows for finer-grained filtering of output from the tests themselves and from the Tracker daemons. Some test code is changed to support the new model.
Diffstat (limited to 'utils')
-rwxr-xr-xutils/sandbox/tracker-sandbox.py117
-rw-r--r--utils/trackertestutils/dbusdaemon.py198
-rw-r--r--utils/trackertestutils/dconf.py69
-rw-r--r--utils/trackertestutils/helpers.py246
-rw-r--r--utils/trackertestutils/meson.build1
5 files changed, 288 insertions, 343 deletions
diff --git a/utils/sandbox/tracker-sandbox.py b/utils/sandbox/tracker-sandbox.py
index cc8ebd786..34ad09ed5 100755
--- a/utils/sandbox/tracker-sandbox.py
+++ b/utils/sandbox/tracker-sandbox.py
@@ -24,6 +24,7 @@
#
import argparse
+import configparser
import locale
import logging
import os
@@ -33,10 +34,10 @@ import subprocess
import sys
import threading
-import configparser
-
from gi.repository import GLib
+import trackertestutils.dbusdaemon
+
# Script
script_name = 'tracker-sandbox'
script_version = '1.0'
@@ -84,116 +85,6 @@ log = logging.getLogger('sandbox')
dbuslog = logging.getLogger('dbus')
-# Private DBus daemon
-
-class DBusDaemon:
- """The private D-Bus instance that provides the sandbox's session bus.
-
- We support reading and writing the session information to a file. This
- means that if the user runs two sandbox instances on the same data
- directory at the same time, they will share the same message bus.
- """
-
- def __init__(self, session_file=None):
- self.session_file = session_file
- self.existing_session = False
- self.process = None
-
- try:
- self.address, self.pid = self.read_session_file(session_file)
- self.existing_session = True
- except FileNotFoundError:
- log.debug("No existing D-Bus session file was found.")
-
- self.address = None
- self.pid = None
-
- def get_session_file(self):
- """Returns the path to the session file if we created it, or None."""
- if self.existing_session:
- return None
- return self.session_file
-
- def get_address(self):
- return self.address
-
- @staticmethod
- def read_session_file(session_file):
- with open(session_file, 'r') as f:
- content = f.read()
-
- try:
- address = content.splitlines()[0]
- pid = int(content.splitlines()[1])
- except ValueError:
- raise RuntimeError(f"D-Bus session file {session_file} is not valid. "
- "Remove this file to start a new session.")
-
- return address, pid
-
- @staticmethod
- def write_session_file(session_file, address, pid):
- os.makedirs(os.path.dirname(session_file), exist_ok=True)
-
- content = '%s\n%s' % (address, pid)
- with open(session_file, 'w') as f:
- f.write(content)
-
- def start_if_needed(self):
- if self.existing_session:
- log.debug('Using existing D-Bus session from file "%s" with address "%s"'
- ' with PID %d' % (self.session_file, self.address, self.pid))
- else:
- dbus_command = ['dbus-daemon', '--session', '--print-address=1', '--print-pid=1']
- self.process = subprocess.Popen(dbus_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-
- try:
- self.address = self.process.stdout.readline().strip().decode('ascii')
- self.pid = int(self.process.stdout.readline().strip().decode('ascii'))
- except ValueError:
- error = self.process.stderr.read().strip().decode('unicode-escape')
- raise RuntimeError(f"Failed to start D-Bus daemon.\n{error}")
-
- log.debug("Using new D-Bus session with address '%s' with PID %d",
- self.address, self.pid)
-
- self.write_session_file(self.session_file, self.address, self.pid)
- log.debug("Wrote D-Bus session file at %s", self.session_file)
-
- # We must read from the pipes continuously, otherwise the daemon
- # process will block.
- self._threads=[threading.Thread(target=self.pipe_to_log, args=(self.process.stdout, 'stdout'), daemon=True),
- threading.Thread(target=self.pipe_to_log, args=(self.process.stderr, 'stderr'), daemon=True)]
- self._threads[0].start()
- self._threads[1].start()
-
- def stop(self):
- if self.process:
- log.debug(" Stopping DBus daemon")
- self.process.terminate()
- self.process.wait()
-
- def pipe_to_log(self, pipe, source):
- """This function processes the output from our dbus-daemon instance."""
- while True:
- line_raw = pipe.readline()
-
- if len(line_raw) == 0:
- break
-
- line = line_raw.decode('utf-8').rstrip()
-
- if line.startswith('(tracker-'):
- # We set G_MESSAGES_PREFIXED=all, meaning that all log messages
- # output by Tracker processes have a prefix. Note that
- # g_print() will NOT be captured here.
- dbuslog.info(line)
- else:
- # Log messages from other daemons, including the dbus-daemon
- # itself, go here. Any g_print() messages also end up here.
- dbuslog.debug(line)
-
-
# Environment / Clean up
def environment_unset(dbus):
@@ -260,7 +151,7 @@ def environment_set(index_location, prefix, verbosity=0):
dbus_session_file = os.path.join(
os.environ['XDG_RUNTIME_DIR'], 'dbus-session')
- dbus = DBusDaemon(dbus_session_file)
+ dbus = trackertestutils.dbusdaemon.DBusDaemon(dbus_session_file)
dbus.start_if_needed()
# Important, other subprocesses must use our new bus
diff --git a/utils/trackertestutils/dbusdaemon.py b/utils/trackertestutils/dbusdaemon.py
new file mode 100644
index 000000000..43fe8f146
--- /dev/null
+++ b/utils/trackertestutils/dbusdaemon.py
@@ -0,0 +1,198 @@
+# Copyright (C) 2018,2019, Sam Thursfield <sam@afuera.me.uk>
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+from gi.repository import Gio
+
+import logging
+import os
+import signal
+import subprocess
+import threading
+
+log = logging.getLogger(__name__)
+dbus_stderr_log = logging.getLogger(__name__ + '.stderr')
+dbus_stdout_log = logging.getLogger(__name__ + '.stdout')
+
+
+class DaemonNotStartedError(Exception):
+ pass
+
+
+class DBusDaemon:
+ """The private D-Bus instance that provides the sandbox's session bus.
+
+ We support reading and writing the session information to a file. This
+ means that if the user runs two sandbox instances on the same data
+ directory at the same time, they will share the same message bus.
+
+ """
+
+ def __init__(self, session_file=None):
+ self.session_file = session_file
+ self.existing_session = False
+ self.process = None
+
+ self.address = None
+ self.pid = None
+
+ self._gdbus_connection = None
+ self._previous_sigterm_handler = None
+
+ self._threads = []
+
+ if session_file:
+ try:
+ self.address, self.pid = self.read_session_file(session_file)
+ self.existing_session = True
+ except FileNotFoundError:
+ log.debug("No existing D-Bus session file was found.")
+
+ def get_session_file(self):
+ """Returns the path to the session file if we created it, or None."""
+ if self.existing_session:
+ return None
+ return self.session_file
+
+ def get_address(self):
+ if self.address is None:
+ raise DaemonNotStartedError()
+ return self.address
+
+ def get_connection(self):
+ if self._gdbus_connection is None:
+ raise DaemonNotStartedError()
+ return self._gdbus_connection
+
+ @staticmethod
+ def read_session_file(session_file):
+ with open(session_file, 'r') as f:
+ content = f.read()
+
+ try:
+ address = content.splitlines()[0]
+ pid = int(content.splitlines()[1])
+ except ValueError:
+ raise RuntimeError(f"D-Bus session file {session_file} is not valid. "
+ "Remove this file to start a new session.")
+
+ return address, pid
+
+ @staticmethod
+ def write_session_file(session_file, address, pid):
+ os.makedirs(os.path.dirname(session_file), exist_ok=True)
+
+ content = '%s\n%s' % (address, pid)
+ with open(session_file, 'w') as f:
+ f.write(content)
+
+ def start_if_needed(self, config_file=None, env=None):
+ if self.existing_session:
+ log.debug('Using existing D-Bus session from file "%s" with address "%s"'
+ ' with PID %d' % (self.session_file, self.address, self.pid))
+ else:
+ dbus_command = ['dbus-daemon', '--print-address=1', '--print-pid=1']
+ if config_file:
+ dbus_command += ['--config-file=' + config_file]
+ else:
+ dbus_command += ['--session']
+ log.debug("Running: %s", dbus_command)
+ self.process = subprocess.Popen(
+ dbus_command, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ self._previous_sigterm_handler = signal.signal(
+ signal.SIGTERM, self._sigterm_handler)
+
+ try:
+ self.address = self.process.stdout.readline().strip().decode('ascii')
+ self.pid = int(self.process.stdout.readline().strip().decode('ascii'))
+ except ValueError:
+ error = self.process.stderr.read().strip().decode('unicode-escape')
+ raise RuntimeError(f"Failed to start D-Bus daemon.\n{error}")
+
+ log.debug("Using new D-Bus session with address '%s' with PID %d",
+ self.address, self.pid)
+
+ if self.session_file:
+ self.write_session_file(self.session_file, self.address, self.pid)
+ log.debug("Wrote D-Bus session file at %s", self.session_file)
+
+ # We must read from the pipes continuously, otherwise the daemon
+ # process will block.
+ self._threads=[threading.Thread(target=self.pipe_to_log, args=(self.process.stdout, dbus_stdout_log), daemon=True),
+ threading.Thread(target=self.pipe_to_log, args=(self.process.stderr, dbus_stdout_log), daemon=True)]
+ self._threads[0].start()
+ self._threads[1].start()
+
+ self._gdbus_connection = Gio.DBusConnection.new_for_address_sync(
+ self.address,
+ Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT |
+ Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION, None, None)
+
+ log.debug("Pinging the new D-Bus daemon...")
+ self.ping_sync()
+
+ def stop(self):
+ if self.process:
+ log.debug(" Stopping DBus daemon")
+ self.process.terminate()
+ self.process.wait()
+ self.process = None
+ if len(self._threads) > 0:
+ log.debug(" Stopping %i pipe reader threads", len(self._threads))
+ for thread in self._threads:
+ thread.join()
+ self.threads = []
+ if self._previous_sigterm_handler:
+ signal.signal(signal.SIGTERM, self._previous_sigterm_handler)
+ self._previous_sigterm_handler = None
+
+ def pipe_to_log(self, pipe, dbuslog):
+ """This function processes the output from our dbus-daemon instance."""
+ while True:
+ line_raw = pipe.readline()
+
+ if len(line_raw) == 0:
+ break
+
+ line = line_raw.decode('utf-8').rstrip()
+
+ if line.startswith('(tracker-'):
+ # We set G_MESSAGES_PREFIXED=all, meaning that all log messages
+ # output by Tracker processes have a prefix. Note that
+ # g_print() will NOT be captured here.
+ dbuslog.info(line)
+ else:
+ # Log messages from other daemons, including the dbus-daemon
+ # itself, go here. Any g_print() messages also end up here.
+ dbuslog.debug(line)
+ log.debug("Thread stopped")
+
+ # I'm not sure why this is needed, or if it's correct, but without it
+ # we see warnings like this:
+ #
+ # ResourceWarning: unclosed file <_io.BufferedReader name=3>
+ pipe.close()
+
+ def _sigterm_handler(self, signal, frame):
+ log.info("Received signal %s", signal)
+ self.stop()
+
+ def ping_sync(self):
+ self._gdbus_connection.call_sync(
+ 'org.freedesktop.DBus', '/', 'org.freedesktop.DBus', 'GetId',
+ None, None, Gio.DBusCallFlags.NONE, 10000, None)
diff --git a/utils/trackertestutils/dconf.py b/utils/trackertestutils/dconf.py
index 4ad0e88e9..fe6d981fb 100644
--- a/utils/trackertestutils/dconf.py
+++ b/utils/trackertestutils/dconf.py
@@ -18,11 +18,9 @@
# 02110-1301, USA.
#
-from gi.repository import GLib
-from gi.repository import Gio
-
import logging
import os
+import subprocess
log = logging.getLogger(__name__)
@@ -36,28 +34,23 @@ class DConfClient(object):
this reason, and the constructor will fail if this isn't the profile in
use, to avoid any risk of modifying or removing your real configuration.
- The constructor will fail if DConf is not the default backend, because this
- probably indicates that the memory backend is in use. Without DConf the
- required configuration changes will not take effect, causing many tests to
- break.
+ We use the `gsettings` binary rather than using the Gio.Settings API.
+ This is to avoid the need to set DCONF_PROFILE in our own process
+ environment.
"""
- def __init__(self, schema):
- self._settings = Gio.Settings.new(schema)
-
- backend = self._settings.get_property('backend')
- self._check_settings_backend_is_dconf(backend)
- self._check_using_correct_dconf_profile()
-
- def _check_settings_backend_is_dconf(self, backend):
- typename = type(backend).__name__.split('.')[-1]
- if typename != 'DConfSettingsBackend':
- raise Exception(
- "The functional tests require DConf to be the default "
- "GSettings backend. Got %s instead." % typename)
+ def __init__(self, sandbox):
+ self.env = os.environ
+ self.env.update(sandbox.extra_env)
+ self.env['DBUS_SESSION_BUS_ADDRESS'] = sandbox.daemon.get_address()
def _check_using_correct_dconf_profile(self):
- profile = os.environ["DCONF_PROFILE"]
+ profile = self.env.get("DCONF_PROFILE")
+ if not profile:
+ raise Exception(
+ "DCONF_PROFILE is not set in the environment. This class must "
+ "be created inside a TrackerDBussandbox to avoid risk of "
+ "interfering with real settings.")
if not os.path.exists(profile):
raise Exception(
"Unable to find DConf profile '%s'. Check that Tracker and "
@@ -66,35 +59,11 @@ class DConfClient(object):
assert os.path.basename(profile) == "trackertest"
- def write(self, key, value):
+ def write(self, schema, key, value):
"""
Write a settings value.
"""
- self._settings.set_value(key, value)
-
- def read(self, schema, key):
- """
- Read a settings value.
- """
- return self._settings.get_value(key)
-
- def reset(self):
- """
- Remove all stored values, resetting configuration to the default.
-
- This can be done by removing the entire 'trackertest' configuration
- database.
- """
-
- self._check_using_correct_dconf_profile()
-
- # XDG_CONFIG_HOME is useless, so we use HOME. This code should not be
- # needed unless for some reason the test is not being run via the
- # 'test-runner.sh' script.
- dconf_db = os.path.join(os.environ["HOME"],
- ".config",
- "dconf",
- "trackertest")
- if os.path.exists(dconf_db):
- log.debug("[Conf] Removing dconf database: %s", dconf_db)
- os.remove(dconf_db)
+ subprocess.run(['gsettings', 'set', schema, key, value.print_(False)],
+ env=self.env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
diff --git a/utils/trackertestutils/helpers.py b/utils/trackertestutils/helpers.py
index e6219be3c..45fa67242 100644
--- a/utils/trackertestutils/helpers.py
+++ b/utils/trackertestutils/helpers.py
@@ -24,8 +24,8 @@ from gi.repository import GLib
import atexit
import logging
import os
-import subprocess
+from . import dbusdaemon
from . import mainloop
log = logging.getLogger(__name__)
@@ -54,172 +54,9 @@ def _cleanup_processes():
atexit.register(_cleanup_processes)
-class Helper:
+class StoreHelper():
"""
- Abstract helper for Tracker processes. Launches the process
- and waits for it to appear on the session bus.
-
- The helper will fail if the process is already running. Use
- test-runner.sh to ensure the processes run inside a separate DBus
- session bus.
-
- The process is watched using a timed GLib main loop source. If the process
- exits with an error code, the test will abort the next time the main loop
- is entered (or straight away if currently running the main loop).
- """
-
- STARTUP_TIMEOUT = 200 # milliseconds
- SHUTDOWN_TIMEOUT = 200 #
-
- def __init__(self, helper_name, bus_name, process_path):
- self.name = helper_name
- self.bus_name = bus_name
- self.process_path = process_path
-
- self.log = logging.getLogger(f'{__name__}.{self.name}')
-
- self.process = None
- self.available = False
-
- self.loop = mainloop.MainLoop()
-
- self.bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
-
- def _start_process(self, command_args=None, extra_env=None):
- global _process_list
- _process_list.append(self)
-
- command = [self.process_path] + (command_args or [])
- self.log.debug("Starting %s.", ' '.join(command))
-
- env = os.environ
- if extra_env:
- self.log.debug(" starting with extra environment: %s", extra_env)
- env.update(extra_env)
-
- try:
- return subprocess.Popen(command, env=env)
- except OSError as e:
- raise RuntimeError("Error starting %s: %s" % (self.process_path, e))
-
- def _bus_name_appeared(self, connection, name, owner):
- self.log.debug("%s appeared on the message bus, owned by %s", name, owner)
- self.available = True
- self.loop.quit()
-
- def _bus_name_vanished(self, connection, name):
- self.log.debug("%s vanished from the message bus", name)
- self.available = False
- self.loop.quit()
-
- def _process_watch_cb(self):
- if self.process_watch_timeout == 0:
- # GLib seems to call the timeout after we've removed it
- # sometimes, which causes errors unless we detect it.
- return False
-
- status = self.process.poll()
-
- if status is None:
- return True # continue
- elif status == 0 and not self.abort_if_process_exits_with_status_0:
- return True # continue
- else:
- self.process_watch_timeout = 0
- raise RuntimeError(f"{self.name} exited with status: {self.status}")
-
- def _process_startup_timeout_cb(self):
- self.log.debug(f"Process timeout of {self.STARTUP_TIMEOUT}ms was called")
- self.loop.quit()
- self.timeout_id = None
- return False
-
- def start(self, command_args=None, extra_env=None):
- """
- Start an instance of process and wait for it to appear on the bus.
- """
- if self.process is not None:
- raise RuntimeError("%s: already started" % self.name)
-
- self._bus_name_watch_id = Gio.bus_watch_name_on_connection(
- self.bus, self.bus_name, Gio.BusNameWatcherFlags.NONE,
- self._bus_name_appeared, self._bus_name_vanished)
-
- # We expect the _bus_name_vanished callback to be called here,
- # causing the loop to exit again.
- self.loop.run_checked()
-
- if self.available:
- # It's running, but we didn't start it...
- raise RuntimeError("Unable to start test instance of %s: "
- "already running" % self.name)
-
- self.process = self._start_process(command_args=command_args,
- extra_env=extra_env)
- self.log.debug('Started with PID %i', self.process.pid)
-
- self.process_startup_timeout = GLib.timeout_add(
- self.STARTUP_TIMEOUT, self._process_startup_timeout_cb)
-
- self.abort_if_process_exits_with_status_0 = True
-
- # Run the loop until the bus name appears, or the process dies.
- self.loop.run_checked()
-
- self.abort_if_process_exits_with_status_0 = False
-
- def stop(self):
- global _process_list
-
- if self.process is None:
- # Seems that it didn't even start...
- return
-
- if self.process_startup_timeout != 0:
- GLib.source_remove(self.process_startup_timeout)
- self.process_startup_timeout = 0
-
- if self.process.poll() == None:
- self.process.terminate()
- returncode = self.process.wait(timeout=self.SHUTDOWN_TIMEOUT * 1000)
- if returncode is None:
- self.log.debug("Process failed to terminate in time, sending kill!")
- self.process.kill()
- self.process.wait()
- elif returncode > 0:
- self.log.warn("Process returned error code %s", returncode)
-
- self.log.debug("Process stopped.")
-
- # Run the loop to handle the expected name_vanished signal.
- self.loop.run_checked()
- Gio.bus_unwatch_name(self._bus_name_watch_id)
-
- self.process = None
- _process_list.remove(self)
-
- def kill(self):
- global _process_list
-
- if self.process_watch_timeout != 0:
- GLib.source_remove(self.process_watch_timeout)
- self.process_watch_timeout = 0
-
- self.process.kill()
-
- # Name owner changed callback should take us out from this loop
- self.loop.run_checked()
- Gio.bus_unwatch_name(self._bus_name_watch_id)
-
- self.process = None
- _process_list.remove(self)
-
- self.log.debug("Process killed.")
-
-
-class StoreHelper (Helper):
- """
- Helper for starting and testing the tracker-store daemon.
+ Helper for testing the tracker-store daemon.
"""
TRACKER_BUSNAME = 'org.freedesktop.Tracker1'
@@ -235,32 +72,41 @@ class StoreHelper (Helper):
TRACKER_STATUS_OBJ_PATH = "/org/freedesktop/Tracker1/Status"
STATUS_IFACE = "org.freedesktop.Tracker1.Status"
- def __init__(self, process_path):
- Helper.__init__(self, "tracker-store", self.TRACKER_BUSNAME, process_path)
+ def __init__(self, dbus_connection):
+ self.log = logging.getLogger(__name__)
+ self.loop = mainloop.MainLoop()
- def start(self, command_args=None, extra_env=None):
- Helper.start(self, command_args, extra_env)
+ self.bus = dbus_connection
+ self.graph_updated_handler_id = 0
self.resources = Gio.DBusProxy.new_sync(
- self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None,
+ self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION, None,
self.TRACKER_BUSNAME, self.TRACKER_OBJ_PATH, self.RESOURCES_IFACE)
self.backup_iface = Gio.DBusProxy.new_sync(
- self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None,
+ self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION, None,
self.TRACKER_BUSNAME, self.TRACKER_BACKUP_OBJ_PATH, self.BACKUP_IFACE)
self.stats_iface = Gio.DBusProxy.new_sync(
- self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None,
+ self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION, None,
self.TRACKER_BUSNAME, self.TRACKER_STATS_OBJ_PATH, self.STATS_IFACE)
self.status_iface = Gio.DBusProxy.new_sync(
- self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START, None,
+ self.bus, Gio.DBusProxyFlags.DO_NOT_AUTO_START_AT_CONSTRUCTION, None,
self.TRACKER_BUSNAME, self.TRACKER_STATUS_OBJ_PATH, self.STATUS_IFACE)
+ def start_and_wait_for_ready(self):
+ # The daemon is autostarted as soon as a method is called.
+ #
+ # We set a big timeout to avoid interfering when a daemon is being
+ # interactively debugged.
self.log.debug("Calling %s.Wait() method", self.STATUS_IFACE)
- self.status_iface.Wait()
+ self.status_iface.call_sync('Wait', None, Gio.DBusCallFlags.NONE, 1000000, None)
self.log.debug("Ready")
+ def start_watching_updates(self):
+ assert self.graph_updated_handler_id == 0
+
self.reset_graph_updates_tracking()
def signal_handler(proxy, sender_name, signal_name, parameters):
@@ -269,12 +115,13 @@ class StoreHelper (Helper):
self.graph_updated_handler_id = self.resources.connect(
'g-signal', signal_handler)
+ self.log.debug("Watching for updates from Resources interface")
- def stop(self):
- Helper.stop(self)
-
+ def stop_watching_updates(self):
if self.graph_updated_handler_id != 0:
+ self.log.debug("No longer watching for updates from Resources interface")
self.resources.disconnect(self.graph_updated_handler_id)
+ self.graph_updated_handler_id = 0
# A system to follow GraphUpdated and make sure all changes are tracked.
# This code saves every change notification received, and exposes methods
@@ -329,6 +176,7 @@ class StoreHelper (Helper):
"""
assert (self.inserts_match_function == None)
assert (self.class_to_track == None), "Already waiting for resource of type %s" % self.class_to_track
+ assert (self.graph_updated_handler_id != 0), "You must call start_watching_updates() first."
self.class_to_track = rdf_class
@@ -413,6 +261,7 @@ class StoreHelper (Helper):
"""
assert (self.deletes_match_function == None)
assert (self.class_to_track == None)
+ assert (self.graph_updated_handler_id != 0), "You must call start_watching_updates() first."
def find_resource_deletion(deletes_list):
self.log.debug("find_resource_deletion: looking for %i in %s", id, deletes_list)
@@ -444,8 +293,8 @@ class StoreHelper (Helper):
# Run the event loop until the correct notification arrives
try:
self.loop.run_checked()
- except GraphUpdateTimeoutException:
- raise GraphUpdateTimeoutException("Resource %i has not been deleted." % id)
+ except GraphUpdateTimeoutException as e:
+ raise GraphUpdateTimeoutException("Resource %i has not been deleted." % id) from e
self.deletes_match_function = None
self.class_to_track = None
@@ -458,6 +307,7 @@ class StoreHelper (Helper):
assert (self.inserts_match_function == None)
assert (self.deletes_match_function == None)
assert (self.class_to_track == None)
+ assert (self.graph_updated_handler_id != 0), "You must call start_watching_updates() first."
self.log.debug("Await change to %i %s (%i, %i existing)", subject_id, property_uri, len(self.inserts_list), len(self.deletes_list))
@@ -582,3 +432,39 @@ class StoreHelper (Helper):
return False
else:
raise Exception("Something fishy is going on")
+
+
+class TrackerDBusSandbox:
+ """
+ Private D-Bus session bus which executes a sandboxed Tracker instance.
+
+ """
+ def __init__(self, dbus_daemon_config_file, extra_env=None):
+ self.dbus_daemon_config_file = dbus_daemon_config_file
+ self.extra_env = extra_env or {}
+
+ self.daemon = dbusdaemon.DBusDaemon()
+
+ def start(self):
+ env = os.environ
+ env.update(self.extra_env)
+ env['G_MESSAGES_PREFIXED'] = 'all'
+
+ # Precreate runtime dir, to avoid this warning from dbus-daemon:
+ #
+ # Unable to set up transient service directory: XDG_RUNTIME_DIR "/home/sam/tracker-tests/tmp_59i3ev1/run" not available: No such file or directory
+ #
+ xdg_runtime_dir = env.get('XDG_RUNTIME_DIR')
+ if xdg_runtime_dir:
+ os.makedirs(xdg_runtime_dir, exist_ok=True)
+
+ log.info("Starting D-Bus daemon for sandbox.")
+ log.debug("Added environment variables: %s", self.extra_env)
+ self.daemon.start_if_needed(self.dbus_daemon_config_file, env=env)
+
+ def stop(self):
+ log.info("Stopping D-Bus daemon for sandbox.")
+ self.daemon.stop()
+
+ def get_connection(self):
+ return self.daemon.get_connection()
diff --git a/utils/trackertestutils/meson.build b/utils/trackertestutils/meson.build
index 99573e323..a144794d5 100644
--- a/utils/trackertestutils/meson.build
+++ b/utils/trackertestutils/meson.build
@@ -1,5 +1,6 @@
sources = [
'__init__.py',
+ 'dbusdaemon.py',
'dconf.py',
'helpers.py',
'mainloop.py'