Writing tests#
Example: Virtual machines#
from pytest_bluezenv import host_config, Bluetoothd, Bluetoothctl
@host_config(
[Bluetoothd(), Bluetoothctl()],
[Bluetoothd(), Bluetoothctl()],
)
def test_bluetoothctl_pair(hosts):
host0, host1 = hosts
host0.bluetoothctl.send("scan on\n")
host0.bluetoothctl.expect(f"Controller {host0.bdaddr.upper()} Discovering: yes")
host1.bluetoothctl.send("pairable on\n")
host1.bluetoothctl.expect("Changing pairable on succeeded")
host1.bluetoothctl.send("discoverable on\n")
host1.bluetoothctl.expect(f"Controller {host1.bdaddr.upper()} Discoverable: yes")
host0.bluetoothctl.expect(f"Device {host1.bdaddr.upper()}")
host0.bluetoothctl.send(f"pair {host1.bdaddr}\n")
idx, m = host0.bluetoothctl.expect(r"Confirm passkey (\d+).*:")
key = m[0].decode("utf-8")
host1.bluetoothctl.expect(f"Confirm passkey {key}")
host0.bluetoothctl.send("yes\n")
host1.bluetoothctl.send("yes\n")
host0.bluetoothctl.expect("Pairing successful")
The test declares a VM setup with two Qemu instances, where both hosts run bluetoothd and start a bluetoothctl process. The Qemu instances have btvirt virtual BT controllers and can see each other.
The test itself runs on the parent host.
The host0/1.bluetoothctl.* commands invoke RPC calls to one of the the two VM instances. In this case, they are controlling the bluetoothctl process using pexpect library to deal with its command line.
When the test body finishes executing, the test passes. Or, it fails
if any assert statement fails or an error is raised. For example,
above RemoteError due to bluetoothctl not proceeding as expected
in pairing is possible.
The host configuration (bluetoothd + bluetoothctl above) is torn down between test (SIGTERM/SIGKILL sent etc.).
By default the VM instance itself continues running, and may be used for other tests that share the same VM setup.
Generally, the framework automatically orders the tests so that the VM setup does not need to be restarted unless needed.
Example: Host plugin#
The host.bluetoothctl implementation used above is as follows:
import logging
import pexpect
from pytest_bluezenv import HostPlugin, Bluetoothd, find_exe, LogStream
class Bluetoothctl(Pexpect):
# Declare unique plugin name
name = "bluetoothctl"
# Declare plugin dependencies to be loaded first
depends = [Bluetoothd()]
# These run on parent host side:
def __init__(self, subdir, name):
self.exe = find_exe(subdir, name)
def presetup(self, config):
pass
# These run on VM side at setup/teardown:
def setup(self, impl):
self.log = logging.getLogger(self.name)
self.log_stream = LogStream(self.name)
self.ctl = pexpect.spawn(self.exe, logfile=self.log_stream.stream)
def teardown(self):
self.ctl.terminate()
# These define custom RPC methods that can be called
def expect(self, *a, **kw):
ret = self.ctl.expect(*a, **kw)
self.log.debug("match found")
return ret, self.ctl.match.groups()
def send(self, *a, **kw):
return self.ctl.send(*a, **kw)
Host plugins are for injecting code to run on the VM side test hosts. The host plugins have scope of one test. The VM side test framework sends SIGTERM and SIGKILL to all processes in the test process group to reset the state between each test.
The plugins are declared by inheriting from HostPlugin. Their __init__() is supposed to only store declarative configuration on self and runs on parent side early in the test discovery phase. The presetup runs on parent side in test setup phase, before VM environment is started. The plugin can for example do pytest.skip(reason=”something”) to skip the test.
The setup() and teardown() methods run on VM-side at host environment start and end. All other methods can be invoked via RPC by the parent tester, and any values returned by them are passed via RPC back to the parent.
To load a plugin to a VM host, pass it to host_config() in the declaration of a given test.