Comment utiliser pytest pour les tests matériels avec TofuPilot
pytest est le framework de test Python le plus populaire. Vous le connaissez déjà pour les tests logiciels. Vous pouvez aussi l'utiliser pour les tests matériels. Ce guide montre comment structurer les tests matériels avec pytest et envoyer les résultats à TofuPilot.
Pourquoi pytest pour le matériel
| Fonctionnalité | Comment elle aide les tests matériels |
|---|---|
| Fixtures | Configurer les instruments, alimentations, connexions |
| Parametrize | Exécuter le même test sur plusieurs DUT ou configurations |
| Markers | Étiqueter les tests par type (fonctionnel, sécurité, calibration) |
| Assertions | Définir les critères de réussite/échec pour les mesures |
| Plugins | Étendre avec du reporting personnalisé, exécution parallèle |
pytest vous fournit le lanceur de tests. TofuPilot vous fournit la plateforme de données. Ensemble, ils remplacent les scripts ad-hoc et les fichiers Excel.
Test matériel de base avec pytest
import pytest
import pyvisa
from tofupilot import TofuPilotClient
# Fixtures pour la configuration des instruments
@pytest.fixture(scope="session")
def rm():
return pyvisa.ResourceManager()
@pytest.fixture(scope="session")
def dmm(rm):
inst = rm.open_resource("TCPIP::192.168.1.10::INSTR")
yield inst
inst.close()
@pytest.fixture(scope="session")
def psu(rm):
inst = rm.open_resource("TCPIP::192.168.1.11::INSTR")
inst.write("OUTP ON")
yield inst
inst.write("OUTP OFF")
inst.close()
@pytest.fixture(scope="session")
def tofupilot():
return TofuPilotClient()
# Tests
def test_vcc_3v3(dmm, tofupilot):
voltage = float(dmm.query("MEAS:VOLT:DC?"))
assert 3.25 <= voltage <= 3.35, f"Rail 3,3 V hors spécification : {voltage} V"
def test_vcc_1v8(dmm, tofupilot):
voltage = float(dmm.query("MEAS:VOLT:DC?"))
assert 1.75 <= voltage <= 1.85, f"Rail 1,8 V hors spécification : {voltage} V"
def test_idle_current(dmm, tofupilot):
current = float(dmm.query("MEAS:CURR:DC?")) * 1000 # mA
assert 30 <= current <= 60, f"Courant de repos hors spécification : {current} mA"Envoi des résultats pytest à TofuPilot
Option 1 : Envoi dans une fixture (recommandé)
Collectez les mesures pendant les tests et envoyez-les à la fin.
import pytest
from tofupilot import TofuPilotClient
class TestCollector:
"""Collecte les mesures pendant une session de test."""
def __init__(self):
self.measurements = []
self.steps = []
self.all_passed = True
def add_measurement(self, step_name, name, value, unit, limit_low=None, limit_high=None):
passed = True
if limit_low is not None and value < limit_low:
passed = False
if limit_high is not None and value > limit_high:
passed = False
if not passed:
self.all_passed = False
# Trouver ou créer l'étape
step = next((s for s in self.steps if s["name"] == step_name), None)
if not step:
step = {"name": step_name, "step_type": "measurement", "status": True, "measurements": []}
self.steps.append(step)
measurement = {"name": name, "value": value, "unit": unit}
if limit_low is not None:
measurement["limit_low"] = limit_low
if limit_high is not None:
measurement["limit_high"] = limit_high
step["measurements"].append(measurement)
if not passed:
step["status"] = False
@pytest.fixture(scope="session")
def collector():
return TestCollector()
@pytest.fixture(scope="session", autouse=True)
def upload_results(collector):
yield # Exécuter tous les tests
# Envoyer après la fin de tous les tests
client = TofuPilotClient()
serial = input("Entrez le numéro de série du DUT : ") if not hasattr(collector, "serial") else collector.serial
client.create_run(
procedure_id="PYTEST-BOARD-FUNCTIONAL",
unit_under_test={"serial_number": serial},
run_passed=collector.all_passed,
steps=collector.steps,
)def test_vcc_3v3(dmm, collector):
voltage = float(dmm.query("MEAS:VOLT:DC?"))
collector.add_measurement("Power Rails", "vcc_3v3", voltage, "V", limit_low=3.25, limit_high=3.35)
assert 3.25 <= voltage <= 3.35
def test_vcc_1v8(dmm, collector):
voltage = float(dmm.query("MEAS:VOLT:DC?"))
collector.add_measurement("Power Rails", "vcc_1v8", voltage, "V", limit_low=1.75, limit_high=1.85)
assert 1.75 <= voltage <= 1.85
def test_idle_current(dmm, collector):
current = float(dmm.query("MEAS:CURR:DC?")) * 1000
collector.add_measurement("Current Draw", "idle_current_ma", current, "mA", limit_low=30, limit_high=60)
assert 30 <= current <= 60Option 2 : Plugin pytest
Écrivez un plugin pytest qui se branche sur les résultats de test.
import pytest
from tofupilot import TofuPilotClient
def pytest_sessionfinish(session, exitstatus):
"""Envoie les résultats après la fin de tous les tests."""
client = TofuPilotClient()
passed = exitstatus == 0
# Collecter les résultats de la session
steps = []
for item in session.items:
report = item.stash.get("report", None)
if report:
steps.append({
"name": item.name,
"step_type": "measurement",
"status": report.passed,
"measurements": item.stash.get("measurements", []),
})
client.create_run(
procedure_id="PYTEST-FUNCTIONAL",
unit_under_test={"serial_number": session.config.getoption("--serial", "UNKNOWN")},
run_passed=passed,
steps=steps,
)Exécution des tests
# Exécuter tous les tests matériels
pytest test_board.py -v
# Exécuter avec un numéro de série
pytest test_board.py --serial UNIT-5501
# Exécuter uniquement les tests de rails d'alimentation
pytest test_board.py -k "vcc" -v
# Exécuter avec des markers
pytest test_board.py -m "safety" -vOrganiser les tests matériels avec pytest
Utiliser les markers pour les catégories de tests
import pytest
@pytest.mark.safety
def test_hipot(hipot_tester, collector):
"""Test de sécurité - doit réussir pour chaque unité."""
leakage = hipot_tester.run_test(1500)
collector.add_measurement("Safety", "hipot_leakage_ma", leakage, "mA", limit_high=5.0)
assert leakage < 5.0
@pytest.mark.functional
def test_communication(dut, collector):
"""Test fonctionnel - vérifie le fonctionnement de base."""
response = dut.ping()
collector.add_measurement("Communication", "uart_ping", 1 if response else 0, "bool", limit_low=1)
assert response
@pytest.mark.calibration
def test_adc_accuracy(dut, collector):
"""Test de calibration - vérifie la précision des mesures."""
error = dut.measure_adc_error()
collector.add_measurement("Calibration", "adc_error_pct", error, "%", limit_high=0.5)
assert error < 0.5Utiliser parametrize pour les tests multi-canaux
import pytest
@pytest.mark.parametrize("channel,expected_v,tolerance", [
(1, 3.3, 0.05),
(2, 1.8, 0.05),
(3, 5.0, 0.10),
(4, 12.0, 0.20),
])
def test_voltage_rail(dmm, collector, channel, expected_v, tolerance):
voltage = dmm.measure_channel(channel)
collector.add_measurement(
"Power Rails",
f"rail_ch{channel}_v",
voltage,
"V",
limit_low=expected_v - tolerance,
limit_high=expected_v + tolerance,
)
assert abs(voltage - expected_v) < tolerancepytest vs. OpenHTF
| Fonctionnalité | pytest | OpenHTF |
|---|---|---|
| Courbe d'apprentissage | Faible (la plupart des développeurs Python le connaissent) | Moyenne (spécifique au matériel) |
| Fonctionnalités de test matériel | Généraliste | Conçu pour le matériel (phases, plugs, mesures) |
| Interface opérateur | Aucune intégrée | Interface web intégrée |
| Communauté | Très large | Petite mais spécialisée |
| Intégration TofuPilot | Via du code personnalisé | Callback natif |
Utilisez pytest quand votre équipe le connaît déjà et que vous voulez quelque chose de fonctionnel rapidement. Utilisez OpenHTF quand vous avez besoin du framework complet de test matériel avec interface opérateur et phases structurées.
Les deux envoient les données à TofuPilot de la même manière. Les données dans TofuPilot sont identiques quel que soit le framework qui les a générées.