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/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/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..11943e9 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,11 +17,9 @@ rm "$ADDRESS_FILE" "$PID_FILE" PYTHON=${1:-python} -"$PYTHON" -m pydbus.tests.context -"$PYTHON" -m pydbus.tests.identifier +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