[tracker-miners/sam/umockdev] functional-tests: Add miner-power test
- From: Sam Thursfield <sthursfield src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [tracker-miners/sam/umockdev] functional-tests: Add miner-power test
- Date: Mon, 16 Aug 2021 12:37:22 +0000 (UTC)
commit d508ffc51b3631ea1c1057f93c534903f23c394f
Author: Sam Thursfield <sam afuera me uk>
Date: Mon Aug 16 14:03:17 2021 +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/common/skip-test.sh | 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 | 3 +-
tests/functional-tests/meson.build | 30 ++++-
tests/functional-tests/miner-power.py | 89 +++++++++++++
tests/functional-tests/minerhelper.py | 146 +++++++++++----------
tests/meson.build | 6 +-
.../{test-bus.conf.in => test-session-bus.conf.in} | 0
tests/test-system-bus.conf | 20 +++
12 files changed, 323 insertions(+), 75 deletions(-)
---
diff --git a/meson.build b/meson.build
index 8a5bbde65..0869206c5 100644
--- a/meson.build
+++ b/meson.build
@@ -89,6 +89,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/common/skip-test.sh b/tests/common/skip-test.sh
new file mode 100755
index 000000000..fe5eba0d7
--- /dev/null
+++ b/tests/common/skip-test.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+# Magic 'test skipped' code, from https://mesonbuild.com/Reference-manual.html#test
+exit 77
diff --git a/tests/functional-tests/configuration.json.in b/tests/functional-tests/configuration.json.in
index 6a45d8eca..ce2d953a2 100644
--- a/tests/functional-tests/configuration.json.in
+++ b/tests/functional-tests/configuration.json.in
@@ -1,13 +1,14 @@
{
"TEST_CLI_DIR": "@TEST_CLI_DIR@",
"TEST_CLI_SUBCOMMANDS_DIR": "@TEST_CLI_SUBCOMMANDS_DIR@",
- "TEST_DBUS_DAEMON_CONFIG_FILE": "@TEST_DBUS_DAEMON_CONFIG_FILE@",
"TEST_DCONF_PROFILE": "@TEST_DCONF_PROFILE@",
"TEST_DOMAIN_ONTOLOGY_RULE": "@TEST_DOMAIN_ONTOLOGY_RULE@",
"TEST_EXTRACTOR_RULES_DIR": "@TEST_EXTRACTOR_RULES_DIR@",
"TEST_EXTRACTORS_DIR": "@TEST_EXTRACTORS_DIR@",
"TEST_GSETTINGS_SCHEMA_DIR": "@TEST_GSETTINGS_SCHEMA_DIR@",
"TEST_LANGUAGE_STOP_WORDS_DIR": "@TEST_LANGUAGE_STOP_WORDS_DIR@",
+ "TEST_SESSION_BUS_CONFIG_FILE": "@TEST_SESSION_BUS_CONFIG_FILE@",
+ "TEST_SYSTEM_BUS_CONFIG_FILE": "@TEST_SYSTEM_BUS_CONFIG_FILE@",
"TEST_WRITEBACK_MODULES_DIR": "@TEST_WRITEBACK_MODULES_DIR@",
"TEST_TAP_ENABLED": @TEST_TAP_ENABLED@,
"TRACKER_EXTRACT_PATH": "@TRACKER_EXTRACT_PATH@"
diff --git a/tests/functional-tests/configuration.py b/tests/functional-tests/configuration.py
index ec68cc681..545d23169 100644
--- a/tests/functional-tests/configuration.py
+++ b/tests/functional-tests/configuration.py
@@ -38,7 +38,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 ab4724d1a..d01664226 100644
--- a/tests/functional-tests/fixtures.py
+++ b/tests/functional-tests/fixtures.py
@@ -108,7 +108,7 @@ class TrackerMinerTest(ut.TestCase):
extra_env['LANG'] = 'en_GB.utf8'
self.sandbox = trackertestutils.helpers.TrackerDBusSandbox(
- session_bus_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()
@@ -123,7 +123,6 @@ class TrackerMinerTest(ut.TestCase):
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 9ed3be7b5..141a88161 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()
@@ -9,7 +15,6 @@ tracker_extractors_dir = meson.current_build_dir() / '..' / '..' / 'src' / 'trac
testconf.set('TEST_CLI_DIR', tracker_uninstalled_cli_dir)
testconf.set('TEST_CLI_SUBCOMMANDS_DIR', tracker_uninstalled_cli_subcommands_dir)
-testconf.set('TEST_DBUS_DAEMON_CONFIG_FILE', build_root / 'tests' / 'test-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)
@@ -17,6 +22,8 @@ testconf.set('TEST_EXTRACTORS_DIR', tracker_extractors_dir)
testconf.set('TEST_GSETTINGS_SCHEMA_DIR', tracker_miners_uninstalled_gsettings_schema_dir)
testconf.set('TEST_LANGUAGE_STOP_WORDS_DIR', tracker_uninstalled_stop_words_dir)
testconf.set('TEST_ONTOLOGIES_DIR', tracker_uninstalled_nepomuk_ontologies_dir)
+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_WRITEBACK_MODULES_DIR', tracker_uninstalled_writeback_modules_dir)
testconf.set('TEST_TAP_ENABLED', get_option('tests_tap_protocol').to_string())
testconf.set('TRACKER_EXTRACT_PATH', uninstalled_tracker_extract_path)
@@ -154,6 +161,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'
@@ -192,3 +203,20 @@ 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
+else
+ foreach t: umockdev_tests
+ test(t, skip_test,
+ 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 4d2a5c495..718de632d 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,11 +35,80 @@ import configuration
log = logging.getLogger(__name__)
-class WakeupCycleTimeoutException(RuntimeError):
+class AwaitTimeoutException(RuntimeError):
pass
-DEFAULT_TIMEOUT = 10
+def await_status(miner_iface, target_status, timeout=configuration.AWAIT_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.AWAIT_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 ():
@@ -66,78 +137,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=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_location(self, uri, graphs=None, flags=None):
return self.index.IndexLocation('(sasas)', uri, graphs or [], flags or [])
diff --git a/tests/meson.build b/tests/meson.build
index b559274ca..ddf8cfa8f 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,3 +1,5 @@
+skip_test = find_program('common/skip-test.sh')
+
subdir('common')
subdir('libtracker-miners-common')
@@ -10,8 +12,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]