From 784252c07bf5d7e2ace4035db453967434e1f88a Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Wed, 2 Aug 2023 06:47:44 +0200 Subject: [PATCH] Add initial pytest support Add a new dbusmock.pytest_fixtures module which exports an initial set of fixtures. These mostly just wrap DbusTestCase into a fixture, plus some convenience functions for starting a system or session bus. In the future we may have more fixtures to spawn templates, etc. Ensure that all of our unit tests run with pytest. As that is expensive, only run it for TEST_CODE=1 in Fedora stable. On the others, only run the basic TestAPI checks and the fixtures checks. This was co-developed with Peter Hutterer @whot, thank you! --- README.md | 83 ++++++++++++++++++++++++++++++++-- dbusmock/pytest_fixtures.py | 43 ++++++++++++++++++ packaging/python-dbusmock.spec | 1 + tests/conftest.py | 1 + tests/run-debian | 3 +- tests/run-fedora | 11 ++--- tests/test_api_pytest.py | 47 +++++++++++++++++++ 7 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 dbusmock/pytest_fixtures.py create mode 100644 tests/conftest.py create mode 100644 tests/test_api_pytest.py diff --git a/README.md b/README.md index 5238b6ee..49a0ac32 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ When using a local system/session bus, you can do unit or integration testing without needing root privileges or disturbing a running system. The Python API offers some convenience functions like `start_session_bus()` and `start_system_bus()` for this, in a -`DBusTestCase` class (subclass of the standard `unittest.TestCase`). +`DBusTestCase` class (subclass of the standard `unittest.TestCase`) or +alternatively as a `@pytest.fixture`. You can use this with any programming language, as you can run the mocker as a normal program. The actual setup of the mock (adding @@ -44,7 +45,7 @@ objects, methods, properties, and signals) all happen via D-Bus methods on the `org.freedesktop.DBus.Mock` interface. You just don't have the convenience D-Bus launch API that way. -## Simple example in Python +## Simple example using Python's unittest Picking up the above example about mocking upower's `Suspend()` method, this is how you would set up a mock upower in your test case: @@ -125,6 +126,80 @@ Let's walk through: together with a time stamp; you can use the latter for doing timing related tests, but we just ignore it here. +## Simple example using pytest + +The same functionality as above but instead using the pytest fixture provided +by this package. + +```python +import subprocess + +import dbus +import pytest + +import dbusmock + + +@pytest.fixture +def upower_mock(dbusmock_system): + p_mock = dbusmock_system.spawn_server( + 'org.freedesktop.UPower', + '/org/freedesktop/UPower', + 'org.freedesktop.UPower', + system_bus=True, + stdout=subprocess.PIPE) + + # Get a proxy for the UPower object's Mock interface + dbus_upower_mock = dbus.Interface(dbusmock_system.get_dbus(True).get_object( + 'org.freedesktop.UPower', + '/org/freedesktop/UPower' + ), dbusmock.MOCK_IFACE) + dbus_upower_mock.AddMethod('', 'Suspend', '', '', '') + + yield p_mock + + p_mock.stdout.close() + p_mock.terminate() + p_mock.wait() + + +def test_suspend_on_idle(upower_mock): + # run your program in a way that should trigger one suspend call + + # now check the log that we got one Suspend() call + assert upower_mock.stdout.readline() == b'^[0-9.]+ Suspend$' +``` + +Let's walk through: + +- We import the `system_mock` fixture from dbusmock which provides us + with a system bus started for our test case wherever the + `dbusmock_system` argument is used by a test case and/or a pytest + fixture. + +- The `upower_mock` fixture spawns the mock D-Bus server process for an initial + `/org/freedesktop/UPower` object with an `org.freedesktop.UPower` + D-Bus interface on the system bus. We capture its stdout to be + able to verify that methods were called. + + We then call `org.freedesktop.DBus.Mock.AddMethod()` to add a + `Suspend()` method to our new object to the default D-Bus + interface. This will not do anything (except log its call to + stdout). It takes no input arguments, returns nothing, and does + not run any custom code. + + This mock server process is yielded to the test function that uses + the `upower_mock` fixture - once the test is complete the process is + terminated again. + +- `test_suspend_on_idle()` is the actual test case. It needs to run + your program in a way that should trigger one suspend call. Your + program will try to call `Suspend()`, but as that's now being + served by our mock instead of upower, there will not be any actual + machine suspend. Our mock process will log the method call + together with a time stamp; you can use the latter for doing + timing related tests, but we just ignore it here. + ## Simple example from shell We use the actual session bus for this example. You can use @@ -301,9 +376,7 @@ the mock server as a program. python-dbusmock is hosted on https://github.com/martinpitt/python-dbusmock -Run the unit tests with - - python3 -m unittest +Run the unit tests with `python3 -m unittest` or `pytest`. In CI, the unit tests run in containers. You can run them locally with e.g. diff --git a/dbusmock/pytest_fixtures.py b/dbusmock/pytest_fixtures.py new file mode 100644 index 00000000..515209a2 --- /dev/null +++ b/dbusmock/pytest_fixtures.py @@ -0,0 +1,43 @@ +'''pytest fixtures for DBusMock''' + +# This program 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 3 of the License, or (at your option) any +# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text +# of the license. + +__author__ = 'Martin Pitt' +__copyright__ = '(c) 2023 Martin Pitt ' + +from typing import Iterator + +import pytest + +import dbusmock.testcase + + +@pytest.fixture(name='dbusmock_test', scope='session') +def fixture_dbusmock_test() -> Iterator[dbusmock.testcase.DBusTestCase]: + '''Export the whole DBusTestCase as a fixture.''' + + testcase = dbusmock.testcase.DBusTestCase() + testcase.setUp() + yield testcase + testcase.tearDown() + testcase.tearDownClass() + + +@pytest.fixture(scope='session') +def dbusmock_system(dbusmock_test) -> dbusmock.testcase.DBusTestCase: + '''Export the whole DBusTestCase as a fixture, with the system bus started''' + + dbusmock_test.start_system_bus() + return dbusmock_test + + +@pytest.fixture(scope='session') +def dbusmock_session(dbusmock_test) -> dbusmock.testcase.DBusTestCase: + '''Export the whole DBusTestCase as a fixture, with the session bus started''' + + dbusmock_test.start_session_bus() + return dbusmock_test diff --git a/packaging/python-dbusmock.spec b/packaging/python-dbusmock.spec index 31d79ece..22532fbd 100644 --- a/packaging/python-dbusmock.spec +++ b/packaging/python-dbusmock.spec @@ -15,6 +15,7 @@ BuildRequires: python3-dbus BuildRequires: python3-devel BuildRequires: python3-setuptools BuildRequires: python3-gobject +BuildRequires: python3-pytest BuildRequires: dbus-x11 BuildRequires: upower diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..ab7ecbaf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = "dbusmock.pytest_fixtures" diff --git a/tests/run-debian b/tests/run-debian index 04d9ba9d..093b7ac2 100644 --- a/tests/run-debian +++ b/tests/run-debian @@ -13,7 +13,7 @@ eatmydata apt-get -y --purge dist-upgrade # install build dependencies eatmydata apt-get install --no-install-recommends -y git \ python3-all python3-setuptools python3-setuptools-scm python3-build python3-venv \ - python3-dbus python3-gi gir1.2-glib-2.0 \ + python3-dbus python3-pytest python3-gi gir1.2-glib-2.0 \ dbus libnotify-bin upower network-manager bluez ofono ofono-scripts # systemd's tools otherwise fail on "not been booted with systemd" @@ -27,6 +27,7 @@ export TEST_CODE="$TEST_CODE" cp -r $(pwd) /tmp/source cd /tmp/source python3 -m unittest -v +python3 -m pytest -vv -k 'test_pytest or TestAPI' # massively parallel test to check for races for i in \$(seq 100); do ( PYTHONPATH=. python3 tests/test_api.py TestTemplates || touch /tmp/fail ) & diff --git a/tests/run-fedora b/tests/run-fedora index 7f0ed48b..64a7b80b 100644 --- a/tests/run-fedora +++ b/tests/run-fedora @@ -2,7 +2,7 @@ set -eux # install build dependencies dnf -y install python3-setuptools python3 python3-gobject-base \ - python3-dbus dbus-x11 util-linux \ + python3-dbus python3-pytest dbus-x11 util-linux \ upower NetworkManager bluez libnotify polkit if ! grep -q :el /etc/os-release; then @@ -19,12 +19,11 @@ mkdir -p /run/systemd/system # run build and test as user useradd build -su -s /bin/sh - build << EOF +su -s /bin/sh - build << EOF || { [ -z "$DEBUG" ] || sleep infinity; exit 1; } set -ex cd /source export TEST_CODE="$TEST_CODE" -python3 -m unittest -v || { - [ -z "$DEBUG" ] || sleep infinity - exit 1 -} + +python3 -m unittest -v +python3 -m pytest -v EOF diff --git a/tests/test_api_pytest.py b/tests/test_api_pytest.py new file mode 100644 index 00000000..7c9b601d --- /dev/null +++ b/tests/test_api_pytest.py @@ -0,0 +1,47 @@ +# This program 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 3 of the License, or (at your option) any +# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text +# of the license. + +__author__ = 'Martin Pitt' +__copyright__ = ''' +(c) 2023 Martin Pitt +''' + +import subprocess +import tempfile + +import pytest + +import dbusmock + + +def test_dbusmock_test_spawn_server(dbusmock_session): + test_iface = 'org.freedesktop.Test.Main' + + p_mock = dbusmock_session.spawn_server( + 'org.freedesktop.Test', '/', test_iface, stdout=tempfile.TemporaryFile()) + + obj_test = dbusmock_session.get_dbus().get_object('org.freedesktop.Test', '/') + + obj_test.AddMethod('', 'Upper', 's', 's', 'ret = args[0].upper()', interface_name=dbusmock.MOCK_IFACE) + assert obj_test.Upper('hello', interface=test_iface) == 'HELLO' + + p_mock.terminate() + p_mock.wait() + + +@pytest.fixture(name='upower_mock') +def fixture_upower_mock(dbusmock_system): + p_mock, obj = dbusmock_system.spawn_server_template('upower', stdout=subprocess.DEVNULL) + yield obj + p_mock.terminate() + p_mock.wait() + + +def test_dbusmock_test_spawn_system_template(upower_mock): + assert upower_mock + out = subprocess.check_output(['upower', '--dump'], universal_newlines=True) + assert 'version:' in out + assert '0.99' in out