Initial support for a hardware test harness with pytest and UV

This commit is contained in:
Lorow
2025-11-23 17:15:26 +01:00
parent 7d2eedf5f9
commit 00c4fe66c4
12 changed files with 440 additions and 142 deletions

3
tests/.env.example Normal file
View File

@@ -0,0 +1,3 @@
WIFI_SSID=
WIFI_PASS=
SWITCH_MODE_REBOOT_TIME=10

0
tests/__init__.py Normal file
View File

165
tests/conftest.py Normal file
View File

@@ -0,0 +1,165 @@
import dotenv
import pytest
import time
from tests.utils import (
OpenIrisDeviceManager,
has_command_failed,
get_current_ports,
get_new_port,
)
board_capabilities = {
"esp_eye": ["wired", "wireless"],
"esp32AIThinker": ["wireless"],
"esp32Cam": ["wireless"],
"esp32M5Stack": ["wireless"],
"facefocusvr_eye_L": ["wired", "measure_current"],
"facefocusvr_eye_R": ["wired", "measure_current"],
"facefocusvr_face": ["wired", "measure_current"],
"project_babble": ["wireless", "wired"],
"seed_studio": ["wireless", "wired"],
"wrooms3": ["wireless", "wired"],
"wrooms3QIO": ["wireless", "wired"],
"wrover": ["wireless", "wired"],
}
def pytest_addoption(parser):
parser.addoption("--board", action="store")
parser.addoption(
"--connection",
action="store",
help="Defines how to connect to the given board, wireless by ip/mdns or wired by com/cdc",
)
def pytest_configure(config):
config.addinivalue_line(
"markers", "has_capability(cap): skip if the board does not have the capability"
)
@pytest.fixture(autouse=True)
def check_capability_marker(request, board_lacks_capability):
"""
Autorun on each test, checks if the board we started with, has the required capability
This lets us skip tests that are impossible to run on some boards - like for example:
It's impossible to run wired tests on a wireless board
It's impossible to run tests for measuring current on boards without this feature
"""
if marker := request.node.get_closest_marker("has_capability"):
if not len(marker.args):
raise ValueError(
"has_capability marker must be provided with a capability to check"
)
required_capability = marker.args[0]
if board_lacks_capability(required_capability):
pytest.skip(f"Board does not have capability {required_capability}")
@pytest.fixture(scope="session", autouse=True)
def board_name(request):
board_name = request.config.getoption("--board")
if not board_name:
raise ValueError("No board defined")
yield board_name
@pytest.fixture()
def board_lacks_capability(board_name):
def func(capability: str):
if board_name:
if board_name not in board_capabilities:
raise ValueError(f"Unknown board {board_name}")
return capability not in board_capabilities[board_name]
return True
return func
@pytest.fixture(scope="session", autouse=True)
def board_connection(request):
"""
Grabs the specified connection connection method, to be used ONLY for the initial connection. Everything after it HAS to be handled via Device Manager.
Ports WILL change throughout the tests, device manager can keep track of that and reconnect the board as needed.
"""
board_connection = request.config.getoption("--connection")
if not board_connection:
raise ValueError("No connection method defined")
yield board_connection
@pytest.fixture(scope="session")
def config():
config = dotenv.dotenv_values()
yield config
@pytest.fixture(scope="session")
def openiris_device_manager(board_connection, config):
manager = OpenIrisDeviceManager()
manager.get_device(board_connection, config)
yield manager
if manager._device:
manager._device.disconnect()
@pytest.fixture()
def get_openiris_device(openiris_device_manager):
def func():
return openiris_device_manager.get_device()
return func
@pytest.fixture()
def ensure_board_in_mode(openiris_device_manager, config):
"""
Given the OpenIrisDevice manager, grabs the current device and ensures it's in the required mode
if not, sends the command to switch and attempts reconnection if necessary, returning the device back
"""
supported_modes = ["wifi", "uvc"]
def func(mode, device):
if mode not in supported_modes:
raise ValueError(f"{mode} is not a supported mode")
command_result = device.send_command("get_device_mode")
if has_command_failed(command_result):
raise ValueError(f"Failed to get device mode, error: {command_result}")
current_mode = command_result["results"][0]["result"]["data"]["mode"].lower()
if mode == current_mode:
return device
old_ports = get_current_ports()
command_result = device.send_command("switch_mode", {"mode": mode})
if has_command_failed(command_result):
raise ValueError("Failed to switch mode, rerun the tests")
print("Rebooting the board after changing mode")
device.send_command("restart_device")
sleep_timeout = int(config["SWITCH_MODE_REBOOT_TIME"])
print(
f"Sleeping for {sleep_timeout} seconds to allow the device to switch modes and boot up"
)
time.sleep(sleep_timeout)
new_ports = get_current_ports()
new_device = openiris_device_manager.get_device(
get_new_port(old_ports, new_ports), config
)
return new_device
return func

View File

@@ -0,0 +1,11 @@
from tests.utils import has_command_failed
import pytest
@pytest.mark.has_capability("wired")
def test_ping_wired(get_openiris_device, ensure_board_in_mode):
device = get_openiris_device()
device = ensure_board_in_mode("wifi", device)
command_result = device.send_command("ping")
assert not has_command_failed(command_result)

49
tests/utils.py Normal file
View File

@@ -0,0 +1,49 @@
import time
import serial.tools.list_ports
from tools.openiris_device import OpenIrisDevice
OPENIRIS_DEVICE = None
class OpenIrisDeviceManager:
def __init__(self):
self._device: OpenIrisDevice | None = None
self._current_port: str | None = None
def get_device(self, port: str | None = None, config=None) -> OpenIrisDevice:
"""
Returns the current OpenIrisDevice connection helper
if the port changed from the one given previously, it will attempt to reconnect
if no device exists, we will create one and try to connect
This helper is designed to be used within a session long fixture
"""
if not port and not self._device:
raise ValueError("No device connected yet, provide a port first")
if port and port != self._current_port:
print(f"Port changed from {self._current_port} to {port}, reconnecting...")
self._current_port = port
if self._device:
self._device.disconnect()
self._device = None
self._device = OpenIrisDevice(port, False, False)
self._device.connect()
time.sleep(int(config["SWITCH_MODE_REBOOT_TIME"]))
return self._device
def has_command_failed(result) -> bool:
return "error" in result or result["results"][0]["result"]["status"] != "success"
def get_current_ports() -> list[str]:
return [port.name for port in serial.tools.list_ports.comports()]
def get_new_port(old_ports, new_ports) -> str:
return list(set(new_ports) - set(old_ports))[0]