[tracker-miners/sam/umockdev: 39/39] functional-tests: Add miner-power test



commit 3cebb2ba75a5eba6b9de9705286f408b9e2d30f6
Author: Sam Thursfield <sam afuera me uk>
Date:   Fri May 22 22:08:19 2020 +0200

    functional-tests: Add miner-power test
    
    This test uses [umockdev] to verify that the miner-fs pauses when upower
    reports a low battery condition.
    
    The umockdev dependency is optional and the test will report as being
    skipped if umockdev isn't available.
    
    Depends on https://gitlab.gnome.org/GNOME/tracker-oci-images/-/merge_requests/23
    and https://gitlab.gnome.org/GNOME/tracker/-/merge_requests/254
    
    [umockdev]: https://github.com/martinpitt/umockdev

 meson.build                                        |   3 +
 tests/functional-tests/configuration.json.in       |   3 +-
 tests/functional-tests/configuration.py            |   3 +-
 tests/functional-tests/devices.py                  |  92 +++++++++++++
 tests/functional-tests/fixtures.py                 |   9 +-
 tests/functional-tests/meson.build                 |  25 +++-
 tests/functional-tests/miner-power.py              |  89 +++++++++++++
 tests/functional-tests/minerhelper.py              | 147 +++++++++++----------
 tests/meson.build                                  |   4 +-
 .../{test-bus.conf.in => test-session-bus.conf.in} |   0
 tests/test-system-bus.conf                         |  20 +++
 11 files changed, 318 insertions(+), 77 deletions(-)
---
diff --git a/meson.build b/meson.build
index 89e1e24bb..55409fb5d 100644
--- a/meson.build
+++ b/meson.build
@@ -93,6 +93,9 @@ libmath = cc.find_library('m', required: false)
 network_manager = dependency('libnm', required: get_option('network_manager'))
 have_network_manager = network_manager.found()
 
+# We use this in the functional-tests if available.
+umockdev = dependency('umockdev-1.0', required: false)
+
 have_tracker_extract = get_option('extract')
 have_tracker_miner_fs = get_option('miner_fs')
 have_tracker_miner_rss = get_option('miner_rss')
diff --git a/tests/functional-tests/configuration.json.in b/tests/functional-tests/configuration.json.in
index 7ef895fe7..a4cb8aa33 100644
--- a/tests/functional-tests/configuration.json.in
+++ b/tests/functional-tests/configuration.json.in
@@ -1,5 +1,6 @@
 {
-    "TEST_DBUS_DAEMON_CONFIG_FILE": "@TEST_DBUS_DAEMON_CONFIG_FILE@",
+    "TEST_SESSION_BUS_CONFIG_FILE": "@TEST_SESSION_BUS_CONFIG_FILE@",
+    "TEST_SYSTEM_BUS_CONFIG_FILE": "@TEST_SYSTEM_BUS_CONFIG_FILE@",
     "TEST_DCONF_PROFILE": "@TEST_DCONF_PROFILE@",
     "TEST_DOMAIN_ONTOLOGY_RULE": "@TEST_DOMAIN_ONTOLOGY_RULE@",
     "TEST_EXTRACTOR_RULES_DIR": "@TEST_EXTRACTOR_RULES_DIR@",
diff --git a/tests/functional-tests/configuration.py b/tests/functional-tests/configuration.py
index 3f3d96c0e..d4d9d5452 100644
--- a/tests/functional-tests/configuration.py
+++ b/tests/functional-tests/configuration.py
@@ -42,7 +42,8 @@ with open(os.environ['TRACKER_FUNCTIONAL_TEST_CONFIG']) as f:
     config = json.load(f)
 
 
-TEST_DBUS_DAEMON_CONFIG_FILE = config['TEST_DBUS_DAEMON_CONFIG_FILE']
+TEST_SESSION_BUS_CONFIG_FILE = config['TEST_SESSION_BUS_CONFIG_FILE']
+TEST_SYSTEM_BUS_CONFIG_FILE = config['TEST_SYSTEM_BUS_CONFIG_FILE']
 TRACKER_EXTRACT_PATH = config['TRACKER_EXTRACT_PATH']
 
 
diff --git a/tests/functional-tests/devices.py b/tests/functional-tests/devices.py
new file mode 100644
index 000000000..cab239331
--- /dev/null
+++ b/tests/functional-tests/devices.py
@@ -0,0 +1,92 @@
+# Copyright (C) 2020, 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.
+
+"""
+Mock device helpers for testing.
+"""
+
+
+import logging
+import os
+
+try:
+    import gi
+    gi.require_version('UMockdev', '1.0')
+    from gi.repository import UMockdev
+    HAVE_UMOCKDEV = True
+except ImportError as e:
+    HAVE_UMOCKDEV = False
+    print("Did not find UMockdev library: %s." % e)
+
+log = logging.getLogger(__name__)
+
+
+class UMockdevNotFound(Exception):
+    pass
+
+
+class MockBattery():
+    def __init__(self, testbed):
+        self.testbed = testbed
+        # Mostly copied from
+        # https://github.com/martinpitt/umockdev/blob/master/docs/examples/battery.py
+        self.device = testbed.add_device('power_supply', 'fakeBAT0', None,
+                                         ['type', 'Battery',
+                                          'present', '1',
+                                          'status', 'Discharging',
+                                          'energy_full', '60000000',
+                                          'energy_full_design', '80000000',
+                                          'energy_now', '48000000',
+                                          'voltage_now', '12000000'],
+                                          ['POWER_SUPPLY_ONLINE', '1'])
+
+    def set_battery_power_normal_charge(self):
+        self.testbed.set_attribute(self.device, 'status', 'Discharging')
+        self.testbed.set_attribute(self.device, 'energy_now', '48000000')
+        self.testbed.uevent(self.device, 'change')
+
+    def set_battery_power_low_charge(self):
+        self.testbed.set_attribute(self.device, 'status', 'Discharging')
+        self.testbed.set_attribute(self.device, 'energy_now', '1500000')
+        self.testbed.uevent(self.device, 'change')
+
+    def set_ac_power(self):
+        self.testbed.set_attribute(self.device, 'status', 'Charging')
+        self.testbed.uevent(self.device, 'change')
+
+
+def libumockdev_loaded():
+    """Returns True if the process was run inside `umockdev-wrapper`."""
+    return 'libumockdev-preload' in os.environ['LD_PRELOAD']
+
+
+def create_testbed():
+    if not HAVE_UMOCKDEV:
+        raise UMockdevNotFound()
+    return UMockdev.Testbed.new()
+
+
+def upowerd_path():
+    with open('/usr/share/dbus-1/system-services/org.freedesktop.UPower.service') as f:
+        for line in f:
+            if line.startswith('Exec='):
+                upowerd_path = line.split('=', 1)[1].strip()
+                break
+        else:
+            sys.stderr.write('Cannot determine upowerd path\n')
+            sys.exit(1)
+    return upowerd_path
diff --git a/tests/functional-tests/fixtures.py b/tests/functional-tests/fixtures.py
index 8b0fe892b..c666b0f42 100644
--- a/tests/functional-tests/fixtures.py
+++ b/tests/functional-tests/fixtures.py
@@ -61,9 +61,9 @@ def tracker_test_main():
         # only errors and warnings should be output here unless the environment
         # contains G_MESSAGES_DEBUG= and/or TRACKER_VERBOSITY=1 or more.
         handler_stderr = logging.StreamHandler(stream=sys.stderr)
-        handler_stderr.addFilter(logging.Filter('trackertestutils.dbusdaemon.stderr'))
+        handler_stderr.addFilter(logging.Filter('sandbox-session-bus.stderr'))
         handler_stdout = logging.StreamHandler(stream=sys.stderr)
-        handler_stdout.addFilter(logging.Filter('trackertestutils.dbusdaemon.stdout'))
+        handler_stdout.addFilter(logging.Filter('sandbox-session-bus.stdout'))
         logging.basicConfig(level=logging.INFO,
                             handlers=[handler_stderr, handler_stdout],
                             format='%(message)s')
@@ -96,7 +96,7 @@ class TrackerMinerTest(ut.TestCase):
         extra_env['LANG'] = 'en_GB.utf8'
 
         self.sandbox = trackertestutils.helpers.TrackerDBusSandbox(
-            dbus_daemon_config_file=cfg.TEST_DBUS_DAEMON_CONFIG_FILE, extra_env=extra_env)
+            session_bus_config_file=cfg.TEST_SESSION_BUS_CONFIG_FILE, extra_env=extra_env)
 
         self.sandbox.start()
 
@@ -109,9 +109,8 @@ class TrackerMinerTest(ut.TestCase):
 
             self.sandbox.set_config(self.config())
 
-            self.miner_fs = MinerFsHelper(self.sandbox.get_connection())
+            self.miner_fs = MinerFsHelper(self.sandbox.get_session_bus_connection())
             self.miner_fs.start()
-            self.miner_fs.start_watching_progress()
 
             self.tracker = trackertestutils.helpers.StoreHelper(
                 self.miner_fs.get_sparql_connection())
diff --git a/tests/functional-tests/meson.build b/tests/functional-tests/meson.build
index 8c542efbb..40a815e46 100644
--- a/tests/functional-tests/meson.build
+++ b/tests/functional-tests/meson.build
@@ -1,5 +1,11 @@
 python = find_program('python3')
 
+if umockdev.found()
+  umockdev_wrapper = find_program('umockdev-wrapper')
+else
+  umockdev_wrapper = python
+endif
+
 # Configure functional tests to run completely from source tree.
 testconf = configuration_data()
 
@@ -7,7 +13,8 @@ config_json_full_path = meson.current_build_dir() / 'configuration.json'
 dconf_profile_full_path = meson.current_source_dir() / 'trackertest'
 tracker_extractors_dir = meson.current_build_dir() / '..' / '..' / 'src' / 'tracker-extract'
 
-testconf.set('TEST_DBUS_DAEMON_CONFIG_FILE', build_root / 'tests' / 'test-bus.conf')
+testconf.set('TEST_SESSION_BUS_CONFIG_FILE', build_root / 'tests' / 'test-session-bus.conf')
+testconf.set('TEST_SYSTEM_BUS_CONFIG_FILE', meson.current_source_dir() / '..' / 'test-system-bus.conf')
 testconf.set('TEST_DCONF_PROFILE', dconf_profile_full_path)
 testconf.set('TEST_DOMAIN_ONTOLOGY_RULE', meson.current_build_dir() / 'test-domain.rule')
 testconf.set('TEST_EXTRACTOR_RULES_DIR', tracker_uninstalled_extract_rules_dir)
@@ -148,6 +155,10 @@ else
   warning('No GStreamer h264 codec was detected. Some extractor tests will be disabled.')
 endif
 
+umockdev_tests = [
+  'miner-power',
+]
+
 test_env = environment()
 
 if get_option('tracker_core') == 'subproject'
@@ -183,3 +194,15 @@ foreach t: functional_tests
     suite: ['functional'],
     timeout: 120)
 endforeach
+
+if umockdev.found()
+  # FIXME: these tests don't appear in the test runner output if umockdev
+  # wasn't found. We should really report them as skipped, but it's tricky.
+  foreach t: umockdev_tests
+    file = meson.current_source_dir() / '@0@.py'.format(t)
+    test(t, umockdev_wrapper,
+      args: [python.path(), file],
+      env: test_env,
+      suite: ['functional', 'umockdev'])
+  endforeach
+endif
diff --git a/tests/functional-tests/miner-power.py b/tests/functional-tests/miner-power.py
new file mode 100755
index 000000000..181f7cb70
--- /dev/null
+++ b/tests/functional-tests/miner-power.py
@@ -0,0 +1,89 @@
+# Copyright (C) 2020, Sam Thursfield (sam afuera me uk)
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+# Boston, MA  02110-1301, USA.
+
+"""
+Test that the miner responds to changes in power / battery status.
+"""
+
+import os
+import sys
+
+import configuration
+import devices
+import fixtures
+import minerhelper
+
+import trackertestutils
+
+
+testbed = None
+
+class MinerPowerTest(fixtures.TrackerMinerTest):
+    def setUp(self):
+        # We don't use setUp() from the base class because we need to start
+        # upowerd before the miner-fs.
+
+        extra_env = configuration.test_environment(self.workdir)
+        extra_env['LANG'] = 'en_GB.utf8'
+
+        # This sets the UMOCKDEV_DIR variable in the process environment.
+        #testbed = self.sandbox.get_umockdev_testbed()
+        self.battery = devices.MockBattery(testbed)
+        self.battery.set_battery_power_normal_charge()
+
+        self.sandbox = trackertestutils.helpers.TrackerDBusSandbox(
+            session_bus_config_file=configuration.TEST_SESSION_BUS_CONFIG_FILE,
+            system_bus_config_file=configuration.TEST_SYSTEM_BUS_CONFIG_FILE,
+            extra_env=extra_env)
+
+        try:
+            self.sandbox.start()
+            self.sandbox.system_bus.activate_service('org.freedesktop.UPower', '/org/freedesktop/UPower')
+            self.sandbox.set_config(self.config())
+
+            self.miner_fs = minerhelper.MinerFsHelper(self.sandbox.get_session_bus_connection())
+            self.miner_fs.start()
+        except Exception:
+            self.sandbox.stop()
+            raise
+
+    def test_miner_fs_pause_on_low_battery(self):
+        """The miner-fs should stop indexing if there's a low battery warning."""
+        minerhelper.await_status(self.miner_fs.miner_fs, "Idle")
+
+        with minerhelper.await_signal(self.miner_fs.miner_fs, "Paused"):
+            self.battery.set_battery_power_low_charge()
+        self.assertEqual(self.miner_fs.get_status(), "Paused")
+
+        with minerhelper.await_signal(self.miner_fs.miner_fs, "Resumed"):
+            self.battery.set_ac_power()
+        self.assertEqual(self.miner_fs.get_status(), "Idle")
+
+
+if __name__ == "__main__":
+    if not devices.HAVE_UMOCKDEV:
+        # Return 'skipped' error code so `meson test` reports the test
+        # correctly.
+        sys.exit(77)
+
+    if not devices.libumockdev_loaded():
+        raise RuntimeError("This test must be run inside umockdev-wrapper.")
+
+    # This sets a process-wide environment variable UMOCKDEV_DIR.
+    testbed = devices.create_testbed()
+
+    fixtures.tracker_test_main()
diff --git a/tests/functional-tests/minerhelper.py b/tests/functional-tests/minerhelper.py
index 4aadd97da..01e874c07 100644
--- a/tests/functional-tests/minerhelper.py
+++ b/tests/functional-tests/minerhelper.py
@@ -20,11 +20,13 @@
 
 import gi
 gi.require_version('Tracker', '3.0')
-from gi.repository import Gio, GLib
+from gi.repository import Gio, GLib, GObject
 from gi.repository import Tracker
 
+import contextlib
 import logging
 
+import trackertestutils.dbusdaemon
 import trackertestutils.mainloop
 
 import configuration
@@ -33,10 +35,82 @@ import configuration
 log = logging.getLogger(__name__)
 
 
-class WakeupCycleTimeoutException(RuntimeError):
+class AwaitTimeoutException(RuntimeError):
     pass
 
 
+def await_status(miner_iface, target_status, timeout=configuration.DEFAULT_TIMEOUT):
+    log.info("Blocking until miner reports status of %s", target_status)
+    loop = trackertestutils.mainloop.MainLoop()
+
+    if miner_iface.GetStatus() == target_status:
+        log.info("Status is %s now", target_status)
+        return
+
+    def signal_cb(proxy, sender_name, signal_name, parameters):
+        if signal_name == 'Progress':
+            status, progress, remaining_time = parameters.unpack()
+            log.debug("Got status: %s", status)
+            if status == target_status:
+                loop.quit()
+
+    def timeout_cb():
+        log.info("Timeout fired after %s seconds", timeout)
+        raise AwaitTimeoutException(
+            f"Timeout awaiting miner status of '{target_status}'")
+
+    signal_id = miner_iface.connect('g-signal', signal_cb)
+    timeout_id = GLib.timeout_add_seconds(timeout, timeout_cb)
+
+    loop.run_checked()
+
+    GObject.signal_handler_disconnect(miner_iface, signal_id)
+    GLib.source_remove(timeout_id)
+
+
+class await_signal():
+    """Context manager to await a specific D-Bus signal.
+
+    Useful to wait for org.freedesktop.Tracker3.Miner signals like
+    Paused and Resumed.
+
+    """
+    def __init__(self, miner_iface, signal_name,
+                 timeout=configuration.DEFAULT_TIMEOUT):
+        self.miner_iface = miner_iface
+        self.signal_name = signal_name
+        self.timeout = timeout
+
+        self.loop = trackertestutils.mainloop.MainLoop()
+
+    def __enter__(self):
+        log.info("Awaiting signal %s", self.signal_name)
+
+        def signal_cb(proxy, sender_name, signal_name, parameters):
+            if signal_name == self.signal_name:
+                log.debug("Received signal %s", signal_name)
+                self.loop.quit()
+
+        def timeout_cb():
+            log.info("Timeout fired after %s seconds", self.timeout)
+            raise AwaitTimeoutException(
+                f"Timeout awaiting signal '{self.signal_name}'")
+
+        self.signal_id = self.miner_iface.connect('g-signal', signal_cb)
+        self.timeout_id = GLib.timeout_add_seconds(self.timeout, timeout_cb)
+
+    def __exit__(self, etype, evalue, etraceback):
+        if etype is not None:
+            return False
+
+        self.loop.run_checked()
+
+        GLib.source_remove(self.timeout_id)
+        GObject.signal_handler_disconnect(self.miner_iface, self.signal_id)
+
+        return True
+
+
 class MinerFsHelper ():
 
     MINERFS_BUSNAME = "org.freedesktop.Tracker3.Miner.Files"
@@ -62,78 +136,17 @@ class MinerFsHelper ():
 
     def start(self):
         self.miner_fs.Start()
+        trackertestutils.dbusdaemon.await_bus_name(self.bus, self.MINERFS_BUSNAME)
 
     def stop(self):
         self.miner_fs.Stop()
 
+    def get_status(self):
+        return self.miner_fs.GetStatus()
+
     def get_sparql_connection(self):
         return Tracker.SparqlConnection.bus_new(
             'org.freedesktop.Tracker3.Miner.Files', None, self.bus)
 
-    def start_watching_progress(self):
-        self._previous_status = None
-        self._target_wakeup_count = None
-        self._wakeup_count = 0
-
-        def signal_handler(proxy, sender_name, signal_name, parameters):
-            if signal_name == 'Progress':
-                self._progress_cb(*parameters.unpack())
-
-        self._progress_handler_id = self.miner_fs.connect('g-signal', signal_handler)
-
-    def stop_watching_progress(self):
-        if self._progress_handler_id != 0:
-            self.miner_fs.disconnect(self._progress_handler_id)
-
-    def _progress_cb(self, status, progress, remaining_time):
-        if self._previous_status is None:
-            self._previous_status = status
-        if self._previous_status != 'Idle' and status == 'Idle':
-            self._wakeup_count += 1
-
-        if self._target_wakeup_count is not None and self._wakeup_count >= self._target_wakeup_count:
-            self.loop.quit()
-
-    def wakeup_count(self):
-        """Return the number of wakeup-to-idle cycles the miner-fs completed."""
-        return self._wakeup_count
-
-    def await_wakeup_count(self, target_wakeup_count, timeout=configuration.DEFAULT_TIMEOUT):
-        """Block until the miner has completed N wakeup-and-idle cycles.
-
-        This function is for use by miner-fs tests that should trigger an
-        operation in the miner, but which do not cause a new resource to be
-        inserted. These tests can instead wait for the status to change from
-        Idle to Processing... and then back to Idle.
-
-        The miner may change its status any number of times, but you can use
-        this function reliably as follows:
-
-            wakeup_count = miner_fs.wakeup_count()
-            # Trigger a miner-fs operation somehow ...
-            miner_fs.await_wakeup_count(wakeup_count + 1)
-            # The miner has probably finished processing the operation now.
-
-        If the timeout is reached before enough wakeup cycles complete, an
-        exception will be raised.
-
-        """
-
-        assert self._target_wakeup_count is None
-
-        if self._wakeup_count >= target_wakeup_count:
-            log.debug("miner-fs wakeup count is at %s (target is %s). No need to wait", self._wakeup_count, 
target_wakeup_count)
-        else:
-            def _timeout_cb():
-                raise WakeupCycleTimeoutException()
-            timeout_id = GLib.timeout_add_seconds(timeout, _timeout_cb)
-
-            log.debug("Waiting for miner-fs wakeup count of %s (currently %s)", target_wakeup_count, 
self._wakeup_count)
-            self._target_wakeup_count = target_wakeup_count
-            self.loop.run_checked()
-
-            self._target_wakeup_count = None
-            GLib.source_remove(timeout_id)
-
     def index_file(self, uri):
         return self.index.IndexFile('(s)', uri)
diff --git a/tests/meson.build b/tests/meson.build
index b559274ca..b58e0e15a 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -10,8 +10,8 @@ endif
 subdir('services')
 
 test_bus_conf_file = configure_file(
-  input: 'test-bus.conf.in',
-  output: 'test-bus.conf',
+  input: 'test-session-bus.conf.in',
+  output: 'test-session-bus.conf',
   configuration: conf)
 
 if get_option('functional_tests')
diff --git a/tests/test-bus.conf.in b/tests/test-session-bus.conf.in
similarity index 100%
rename from tests/test-bus.conf.in
rename to tests/test-session-bus.conf.in
diff --git a/tests/test-system-bus.conf b/tests/test-system-bus.conf
new file mode 100644
index 000000000..04e797135
--- /dev/null
+++ b/tests/test-system-bus.conf
@@ -0,0 +1,20 @@
+<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd";>
+<busconfig>
+  <!-- Our well-known bus type, don't change this -->
+  <type>system</type>
+
+  <listen>unix:tmpdir=./</listen>
+
+  <standard_system_servicedirs/>
+
+  <policy context="default">
+    <!-- Allow everything to be sent -->
+    <allow send_destination="*"/>
+    <!-- Allow everything to be received -->
+    <allow eavesdrop="true"/>
+    <!-- Allow anyone to own anything -->
+    <allow own="*"/>
+  </policy>
+
+</busconfig>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]