Skip to content

Commit

Permalink
Add initial pytest support
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
martinpitt committed Aug 20, 2023
1 parent e8233df commit 4cdc1c0
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 13 deletions.
84 changes: 78 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ 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
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:
Expand Down Expand Up @@ -125,6 +125,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_testcase_system):
p_mock = dbusmock_testcase_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_testcase_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_testcase_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
Expand Down Expand Up @@ -301,9 +375,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.

Expand Down
43 changes: 43 additions & 0 deletions dbusmock/pytest_fixtures.py
Original file line number Diff line number Diff line change
@@ -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 <martin@piware.de>'

from typing import Iterator

import pytest

import dbusmock.testcase


@pytest.fixture(name='dbusmock_testcase', scope='session')
def fixture_dbusmock_testcase() -> 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_testcase_system(dbusmock_testcase) -> dbusmock.testcase.DBusTestCase:
'''Export the whole DBusTestCase as a fixture, with system bus started'''

dbusmock_testcase.start_system_bus()
return dbusmock_testcase


@pytest.fixture(scope='session')
def dbusmock_testcase_session(dbusmock_testcase) -> dbusmock.testcase.DBusTestCase:
'''Export the whole DBusTestCase as a fixture, with session bus started'''

dbusmock_testcase.start_session_bus()
return dbusmock_testcase
1 change: 1 addition & 0 deletions packaging/python-dbusmock.spec
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ BuildRequires: python3-dbus
BuildRequires: python3-devel
BuildRequires: python3-setuptools
BuildRequires: python3-gobject
BuildRequires: python3-pytest
BuildRequires: dbus-x11
BuildRequires: upower

Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest_plugins = "dbusmock.pytest_fixtures"
3 changes: 2 additions & 1 deletion tests/run-debian
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 ) &
Expand Down
17 changes: 11 additions & 6 deletions tests/run-fedora
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,12 +19,17 @@ 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
# run all tests with pytest only on TEST_CODE, as that is expensive
if [ -n "$TEST_CODE" ]; then
python3 -m pytest -v
else
python3 -m pytest -vv -k 'test_pytest or TestAPI'
fi
EOF
36 changes: 36 additions & 0 deletions tests/test_api_pytest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import subprocess
import tempfile

import pytest

import dbusmock


def test_dbusmock_testcase_spawn_server(dbusmock_testcase_session):
test_iface = 'org.freedesktop.Test.Main'

p_mock = dbusmock_testcase_session.spawn_server(
'org.freedesktop.Test', '/', test_iface, stdout=tempfile.TemporaryFile())

obj_test = dbusmock_testcase_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_testcase_system):
p_mock, obj = dbusmock_testcase_system.spawn_server_template('upower', stdout=subprocess.DEVNULL)
yield obj
p_mock.terminate()
p_mock.wait()


def test_dbusmock_testcase_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

0 comments on commit 4cdc1c0

Please sign in to comment.