From f1638efdcc6a47eb1a0fd9c6d295bd7a8d7706d1 Mon Sep 17 00:00:00 2001 From: Fabian Neundorf Date: Wed, 19 Oct 2016 09:38:24 +0000 Subject: [PATCH 1/2] Use pytest module Instead of doing the tests manually use a third party library, like `pytest` to make it easier to do and verify tests. --- pydbus/tests/context.py | 53 +++++++++++++++-------------- pydbus/tests/identifier.py | 18 ++++------ tests/py2.7-ubuntu-14.04.dockerfile | 2 +- tests/py2.7-ubuntu-16.04.dockerfile | 2 +- tests/py3.4-ubuntu-14.04.dockerfile | 2 +- tests/py3.5-ubuntu-16.04.dockerfile | 2 +- tests/run.sh | 5 +-- 7 files changed, 40 insertions(+), 44 deletions(-) diff --git a/pydbus/tests/context.py b/pydbus/tests/context.py index 6a6614b..5e0145d 100644 --- a/pydbus/tests/context.py +++ b/pydbus/tests/context.py @@ -1,39 +1,40 @@ from pydbus import SessionBus, connect import os -DBUS_SESSION_BUS_ADDRESS = os.getenv("DBUS_SESSION_BUS_ADDRESS") +import pytest -with connect(DBUS_SESSION_BUS_ADDRESS) as bus: - bus.dbus -del bus._dbus -try: - bus.dbus - assert(False) -except RuntimeError: - pass +def test_remove_dbus(): + DBUS_SESSION_BUS_ADDRESS = os.getenv("DBUS_SESSION_BUS_ADDRESS") + + with connect(DBUS_SESSION_BUS_ADDRESS) as bus: + bus.dbus -with SessionBus() as bus: - pass + del bus._dbus + with pytest.raises(RuntimeError): + bus.dbus -# SessionBus() and SystemBus() are not closed automatically, so this should work: -bus.dbus -with bus.request_name("net.lew21.Test"): - pass +def test_use_exited_bus(): + """Test using a bus instance after its context manager.""" + with SessionBus() as bus: + pass + + # SessionBus() and SystemBus() are not closed automatically, so this should work: + bus.dbus -with bus.request_name("net.lew21.Test"): - pass + with bus.request_name("net.lew21.Test"): + pass -with bus.request_name("net.lew21.Test"): - try: - bus.request_name("net.lew21.Test") - assert(False) - except RuntimeError: + with bus.request_name("net.lew21.Test"): pass -with bus.watch_name("net.lew21.Test"): - pass + with bus.request_name("net.lew21.Test"): + with pytest.raises(RuntimeError): + bus.request_name("net.lew21.Test") -with bus.subscribe(sender="net.lew21.Test"): - pass + with bus.watch_name("net.lew21.Test"): + pass + + with bus.subscribe(sender="net.lew21.Test"): + pass diff --git a/pydbus/tests/identifier.py b/pydbus/tests/identifier.py index e557ee7..5c2032b 100644 --- a/pydbus/tests/identifier.py +++ b/pydbus/tests/identifier.py @@ -1,19 +1,13 @@ -from __future__ import print_function +import pytest + from pydbus.identifier import filter_identifier -import sys -tests = [ +@pytest.mark.parametrize("input, output", [ ("abc", "abc"), ("a_bC", "a_bC"), ("a-b_c", "a_b_c"), ("a@bc", "abc"), ("!@#$%^&*", ""), -] - -ret = 0 -for input, output in tests: - if not filter_identifier(input) == output: - print("ERROR: filter(" + input + ") returned: " + filter_identifier(input), file=sys.stderr) - ret = 1 - -sys.exit(ret) +]) +def test_filter_identifier(input, output): + assert filter_identifier(input) == output diff --git a/tests/py2.7-ubuntu-14.04.dockerfile b/tests/py2.7-ubuntu-14.04.dockerfile index bb61cad..b0ae9b0 100644 --- a/tests/py2.7-ubuntu-14.04.dockerfile +++ b/tests/py2.7-ubuntu-14.04.dockerfile @@ -3,7 +3,7 @@ RUN apt-get update RUN apt-get install -y dbus python-gi python-pip psmisc python-dev RUN python2 --version -RUN pip2 install greenlet +RUN pip2 install greenlet pytest ADD . /root/ RUN cd /root && python2 setup.py install diff --git a/tests/py2.7-ubuntu-16.04.dockerfile b/tests/py2.7-ubuntu-16.04.dockerfile index 450df0e..5d5397f 100644 --- a/tests/py2.7-ubuntu-16.04.dockerfile +++ b/tests/py2.7-ubuntu-16.04.dockerfile @@ -3,7 +3,7 @@ RUN apt-get update RUN apt-get install -y dbus python-gi python-pip psmisc RUN python2 --version -RUN pip2 install greenlet +RUN pip2 install greenlet pytest ADD . /root/ RUN cd /root && python2 setup.py install diff --git a/tests/py3.4-ubuntu-14.04.dockerfile b/tests/py3.4-ubuntu-14.04.dockerfile index 71b1c79..021e971 100644 --- a/tests/py3.4-ubuntu-14.04.dockerfile +++ b/tests/py3.4-ubuntu-14.04.dockerfile @@ -3,7 +3,7 @@ RUN apt-get update RUN apt-get install -y dbus python3-gi python3-pip psmisc python3-dev RUN python3 --version -RUN pip3 install greenlet +RUN pip3 install greenlet pytest ADD . /root/ RUN cd /root && python3 setup.py install diff --git a/tests/py3.5-ubuntu-16.04.dockerfile b/tests/py3.5-ubuntu-16.04.dockerfile index 247fcdc..fdabdc6 100644 --- a/tests/py3.5-ubuntu-16.04.dockerfile +++ b/tests/py3.5-ubuntu-16.04.dockerfile @@ -3,7 +3,7 @@ RUN apt-get update RUN apt-get install -y dbus python3-gi python3-pip psmisc RUN python3 --version -RUN pip3 install greenlet +RUN pip3 install greenlet pytest ADD . /root/ RUN cd /root && python3 setup.py install diff --git a/tests/run.sh b/tests/run.sh index 436c840..3d338b5 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,6 +1,8 @@ #!/bin/sh set -e +cd "$(dirname "$(dirname "$0")")" + ADDRESS_FILE=$(mktemp /tmp/pydbustest.XXXXXXXXX) PID_FILE=$(mktemp /tmp/pydbustest.XXXXXXXXX) @@ -15,8 +17,7 @@ rm "$ADDRESS_FILE" "$PID_FILE" PYTHON=${1:-python} -"$PYTHON" -m pydbus.tests.context -"$PYTHON" -m pydbus.tests.identifier +"$PYTHON" -m pytest -v pydbus/tests/identifier.py pydbus/tests/context.py if [ "$2" != "dontpublish" ] then "$PYTHON" -m pydbus.tests.publish From addf3913368cdc7225039525f3e53ab62b2a0f70 Mon Sep 17 00:00:00 2001 From: Fabian Neundorf Date: Thu, 20 Oct 2016 17:58:22 +0000 Subject: [PATCH 2/2] Use pytest on multi-threaded tests The tests requiring multiple threads (one for publishing the interfaces and one for using them) are largely rewritten to work with pytest. It doesn't support assertions which were not asserted in the main thread. So the threads need to either report the failed assertion or the actual result and that is asserted in the test itself. It also removes any modification of global variables outside the global scope, because that would make it not possible to run multiple tests. Instead there is a new Thread class which automatically quits the main loop after all tests are finished. If the main loop is quit, the main thread can get the status from the threads to check what the result was. If the thread should hang for any reason, it'll timeout after 100 ms and report it as a failure. Also add more tests for the class using multiple interfaces, by using only a specific interface. The test script itself is rewritten to call pytest only once. --- pydbus/tests/publish.py | 51 ++++++------- pydbus/tests/publish_multiface.py | 110 ++++++++++++++++++++-------- pydbus/tests/publish_properties.py | 74 ++++++++----------- pydbus/tests/util.py | 113 +++++++++++++++++++++++++++++ tests/run.sh | 7 +- 5 files changed, 251 insertions(+), 104 deletions(-) create mode 100644 pydbus/tests/util.py diff --git a/pydbus/tests/publish.py b/pydbus/tests/publish.py index 3adbe81..56850a3 100644 --- a/pydbus/tests/publish.py +++ b/pydbus/tests/publish.py @@ -3,10 +3,10 @@ from threading import Thread import sys -done = 0 -loop = GLib.MainLoop() +from pydbus.tests.util import ClientPool, ClientThread -class TestObject(object): + +class DummyObject(object): ''' @@ -23,40 +23,33 @@ def __init__(self, id): def HelloWorld(self, a, b): res = self.id + ": " + a + str(b) - global done - done += 1 - if done == 2: - loop.quit() - print(res) return res -bus = SessionBus() -with bus.publish("net.lew21.pydbus.Test", TestObject("Main"), ("Lol", TestObject("Lol"))): - remoteMain = bus.get("net.lew21.pydbus.Test") - remoteLol = bus.get("net.lew21.pydbus.Test", "Lol") +def test_multiple_requests(): + loop = GLib.MainLoop() + bus = SessionBus() - def t1_func(): - print(remoteMain.HelloWorld("t", 1)) + with bus.publish("net.lew21.pydbus.Test", DummyObject("Main"), ("Lol", DummyObject("Lol"))): + remoteMain = bus.get("net.lew21.pydbus.Test") + remoteLol = bus.get("net.lew21.pydbus.Test", "Lol") - def t2_func(): - print(remoteLol.HelloWorld("t", 2)) + def t1_func(): + return remoteMain.HelloWorld("t", 1) - t1 = Thread(None, t1_func) - t2 = Thread(None, t2_func) - t1.daemon = True - t2.daemon = True + def t2_func(): + return remoteLol.HelloWorld("t", 2) - def handle_timeout(): - print("ERROR: Timeout.") - sys.exit(1) + pool = ClientPool(loop.quit) + t1 = ClientThread(t1_func, loop, pool) + t2 = ClientThread(t2_func, loop, pool) - GLib.timeout_add_seconds(2, handle_timeout) + GLib.timeout_add_seconds(2, loop.quit) - t1.start() - t2.start() + t1.start() + t2.start() - loop.run() + loop.run() - t1.join() - t2.join() + assert t1.result == "Main: t1" + assert t2.result == "Lol: t2" diff --git a/pydbus/tests/publish_multiface.py b/pydbus/tests/publish_multiface.py index 6234032..f566dce 100644 --- a/pydbus/tests/publish_multiface.py +++ b/pydbus/tests/publish_multiface.py @@ -1,12 +1,14 @@ from pydbus import SessionBus from gi.repository import GLib -from threading import Thread +from threading import Thread, Lock import sys +import time -done = 0 -loop = GLib.MainLoop() +import pytest -class TestObject(object): +from pydbus.tests.util import ClientThread + +class DummyObject(object): ''' @@ -21,40 +23,90 @@ class TestObject(object): ''' + + def __init__(self): + self.done = [] + def Method1(self): - global done - done += 1 - if done == 2: - loop.quit() - return "M1" + self.done += ["Method1"] + return self.done[-1] def Method2(self): - global done - done += 1 - if done == 2: - loop.quit() - return "M2" + self.done += ["Method2"] + return self.done[-1] -bus = SessionBus() -with bus.publish("net.lew21.pydbus.tests.expose_multiface", TestObject()): - remote = bus.get("net.lew21.pydbus.tests.expose_multiface") +@pytest.fixture +def defaults(): + loop = GLib.MainLoop() + loop.cancelled = False + bus = SessionBus() - def t1_func(): - print(remote.Method1()) - print(remote.Method2()) + obj = DummyObject() + with bus.publish("net.lew21.pydbus.tests.expose_multiface", obj): + yield loop, obj, bus.get("net.lew21.pydbus.tests.expose_multiface") - t1 = Thread(None, t1_func) - t1.daemon = True - def handle_timeout(): - print("ERROR: Timeout.") - sys.exit(1) +def run(loop, func): + thread = ClientThread(func, loop) + GLib.timeout_add_seconds(2, loop.quit) - GLib.timeout_add_seconds(2, handle_timeout) + thread.start() + loop.run() - t1.start() + try: + return thread.result + except ValueError: + pytest.fail('Unable to finish thread') - loop.run() - t1.join() +def test_using_multiface(defaults): + def thread_func(): + results = [] + results += [remote.Method1()] + results += [remote.Method2()] + return results + + loop, obj, remote = defaults + + result = run(loop, thread_func) + + assert result == ["Method1", "Method2"] + assert obj.done == ["Method1", "Method2"] + + +@pytest.mark.parametrize("interface, method", [ + ("net.lew21.pydbus.tests.Iface1", "Method1"), + ("net.lew21.pydbus.tests.Iface2", "Method2"), +]) +def test_using_specific_interface(defaults, interface, method): + def thread_func(): + return getattr(remote, method)() + + loop, obj, remote = defaults + remote = remote[interface] + + result = run(loop, thread_func) + + assert result == method + assert obj.done == [method] + + +@pytest.mark.parametrize("interface, method", [ + ("net.lew21.pydbus.tests.Iface1", "Method2"), + ("net.lew21.pydbus.tests.Iface2", "Method1"), +]) +def test_using_wrong_interface(defaults, interface, method): + def thread_func(): + with pytest.raises(AttributeError) as e: + getattr(remote, method)() + return e + + loop, obj, remote = defaults + remote = remote[interface] + + result = run(loop, thread_func) + + assert str(result.value) == "'{}' object has no attribute '{}'".format( + interface, method) + assert obj.done == [] diff --git a/pydbus/tests/publish_properties.py b/pydbus/tests/publish_properties.py index aec52e3..71de49b 100644 --- a/pydbus/tests/publish_properties.py +++ b/pydbus/tests/publish_properties.py @@ -3,17 +3,18 @@ from threading import Thread import sys -done = 0 -loop = GLib.MainLoop() +import pytest -class TestObject(object): +from pydbus.tests.util import ClientPool, ClientThread + + +class DummyObject(object): ''' - ''' @@ -21,51 +22,40 @@ def __init__(self): self.Foo = "foo" self.Foobar = "foobar" - def Quit(self): - loop.quit() - -bus = SessionBus() -with bus.publish("net.lew21.pydbus.tests.publish_properties", TestObject()): - remote = bus.get("net.lew21.pydbus.tests.publish_properties") - remote_iface = remote['net.lew21.pydbus.tests.publish_properties'] +def test_properties(): + bus = SessionBus() + loop = GLib.MainLoop() - def t1_func(): - for obj in [remote, remote_iface]: - assert(obj.Foo == "foo") - assert(obj.Foobar == "foobar") - obj.Foobar = "barfoo" - assert(obj.Foobar == "barfoo") - obj.Foobar = "foobar" - assert(obj.Foobar == "foobar") - obj.Bar = "rab" + with bus.publish("net.lew21.pydbus.tests.publish_properties", DummyObject()): + remote = bus.get("net.lew21.pydbus.tests.publish_properties") + remote_iface = remote['net.lew21.pydbus.tests.publish_properties'] - remote.Foobar = "barfoo" + def t1_func(): + for obj in [remote, remote_iface]: + assert(obj.Foo == "foo") + assert(obj.Foobar == "foobar") + obj.Foobar = "barfoo" + assert(obj.Foobar == "barfoo") + obj.Foobar = "foobar" + assert(obj.Foobar == "foobar") + obj.Bar = "rab" - try: - remote.Get("net.lew21.pydbus.tests.publish_properties", "Bar") - assert(False) - except GLib.GError: - pass - try: - remote.Set("net.lew21.pydbus.tests.publish_properties", "Foo", Variant("s", "haxor")) - assert(False) - except GLib.GError: - pass - assert(remote.GetAll("net.lew21.pydbus.tests.publish_properties") == {'Foobar': 'barfoo', 'Foo': 'foo'}) - remote.Quit() + remote.Foobar = "barfoo" - t1 = Thread(None, t1_func) - t1.daemon = True + with pytest.raises(GLib.GError): + remote.Get("net.lew21.pydbus.tests.publish_properties", "Bar") + with pytest.raises(GLib.GError): + remote.Set("net.lew21.pydbus.tests.publish_properties", "Foo", Variant("s", "haxor")) + assert(remote.GetAll("net.lew21.pydbus.tests.publish_properties") == {'Foobar': 'barfoo', 'Foo': 'foo'}) - def handle_timeout(): - print("ERROR: Timeout.") - sys.exit(1) + t1 = ClientThread(t1_func, loop) - GLib.timeout_add_seconds(2, handle_timeout) + GLib.timeout_add_seconds(2, loop.quit) - t1.start() + t1.start() - loop.run() + loop.run() - t1.join() + # The result is not important, but it might reraise the assertion + t1.result diff --git a/pydbus/tests/util.py b/pydbus/tests/util.py new file mode 100644 index 0000000..fa270ab --- /dev/null +++ b/pydbus/tests/util.py @@ -0,0 +1,113 @@ +import time + +from threading import Thread, Lock + + +class TimeLock(object): + + """Lock which can timeout.""" + + def __init__(self): + self._lock = Lock() + + def acquire(self, blocking=True, timeout=-1): + if timeout < 0: + return self._lock.acquire(blocking) + + end_time = time.time() + timeout + while time.time() < end_time: + if self._lock.acquire(False): + return True + else: + time.sleep(0.001) + return False + + def release(self): + return self._lock.release() + + def __enter__(self): + self.acquire() + + def __exit__(self, exc_type, exc_val, exc_tb): + self.release() + + +class ClientPool(object): + + """A pool of threads, which determines if every thread finished.""" + + def __init__(self, finisher): + self._threads = set() + self._finisher = finisher + self._lock = Lock() + + def add(self, thread): + if thread in self._threads: + raise ValueError('Thread was already added') + self._threads.add(thread) + + def finish(self, thread): + with self._lock: + self._threads.remove(thread) + finished = not self._threads + + if finished: + self._finisher() + + +class ClientThread(Thread): + + """ + Thread subclass which is also handling the main loop. + + Each thread can be a part of a pool. If every thread of the pool has + finished, it'll execute some finishing action (usually to quit the loop). + If no pool is defined, it'll make the thread part of it's own pool. + + The return value of the function is saved as the result. If the function + raised an `AssertionError` it'll save that exception. In any case it'll + tell the pool that it finished. + """ + + def __init__(self, func, loop, pool=None): + super(ClientThread, self).__init__(None) + self.daemon = True + self.loop = loop + self.func = func + self._lock = TimeLock() + self._result = None + if pool is None: + pool = ClientPool(loop.quit) + self._pool = pool + if pool is not False: + self._pool.add(self) + + def run(self): + with self._lock: + while not self.loop.is_running: + pass + try: + self._result = self.func() + except AssertionError as e: + self._result = e + finally: + if self._pool is not False: + self._pool.finish(self) + + @property + def result(self): + # If the thread itself quit the loop, it might not have released the + # lock before the main thread already continues to get the result. So + # wait for a bit to actually let the thread finish. + if self._lock.acquire(timeout=0.1): + try: + if isinstance(self._result, AssertionError): + # This will say the error happened here, even though it + # happened inside the thread. + raise self._result + else: + return self._result + finally: + self._lock.release() + else: + raise ValueError() diff --git a/tests/run.sh b/tests/run.sh index 3d338b5..11943e9 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -17,10 +17,9 @@ rm "$ADDRESS_FILE" "$PID_FILE" PYTHON=${1:-python} -"$PYTHON" -m pytest -v pydbus/tests/identifier.py pydbus/tests/context.py +FILES="pydbus/tests/identifier.py pydbus/tests/context.py" if [ "$2" != "dontpublish" ] then - "$PYTHON" -m pydbus.tests.publish - "$PYTHON" -m pydbus.tests.publish_properties - "$PYTHON" -m pydbus.tests.publish_multiface + FILES="$FILES pydbus/tests/publish.py pydbus/tests/publish_properties.py pydbus/tests/publish_multiface.py" fi +"$PYTHON" -m pytest -v $FILES