Déployer des scripts de test sur les stations de production nécessite un processus répétable : fixer les dépendances, empaqueter l'exécutable, configurer l'environnement de chaque station et suivre les versions dans TofuPilot. Ce guide couvre le flux de travail complet, d'un environnement virtuel propre jusqu'à un déploiement versionné sur plusieurs stations.
Prérequis
- Python 3.9 ou supérieur installé sur toutes les stations cibles
- Compte TofuPilot avec au moins une station configurée
- Paquets
tofupilotetopenhtfdisponibles sur PyPI
Étape 1 : Configurer un environnement virtuel
Isolez les dépendances de votre script de test du Python système pour éviter les conflits de version entre les déploiements.
#!/bin/bash
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pipSur les stations Windows :
@echo off
python -m venv .venv
.venv\Scripts\activate
pip install --upgrade pipÉtape 2 : Fixer les dépendances
Fixez toutes les versions des dépendances pour garantir que chaque station exécute le même code. Les dépendances non fixées sont la cause la plus courante des échecs « ça marche sur mon poste » en production.
openhtf==2.1.0
tofupilot==1.5.0
pyserial==3.5
numpy==1.26.4
pyinstaller==6.10.0Installez et vérifiez :
#!/bin/bash
pip install -r requirements.txt
pip freeze > requirements.lock.txt # capturer les dépendances transitives pour auditUtilisez requirements.lock.txt pour l'audit ; utilisez requirements.txt pour les installations. Ne fixez jamais manuellement les dépendances transitives.
Utiliser pyproject.toml (Optionnel)
Si vous gérez vos scripts de test comme un paquet, utilisez pyproject.toml à la place :
[project]
name = "station-tests"
version = "1.4.2"
requires-python = ">=3.9"
dependencies = [
"openhtf==2.1.0",
"tofupilot==1.5.0",
"pyserial==3.5",
"numpy==1.26.4",
]
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"Le champ version dans pyproject.toml devient votre version de déploiement. Incrémentez-le avant chaque release.
Étape 3 : Écrire le script de test
Structurez votre test avec l'injection de plug via le décorateur @htf.plug. N'utilisez pas les annotations de type pour l'injection de plug.
import openhtf as htf
from tofupilot.openhtf import TofuPilot
from plugs.serial_plug import SerialPlug
from plugs.power_plug import PowerPlug
@htf.measures(
htf.Measurement("boot_time_ms")
.in_range(maximum=5000)
.doc("Temps entre mise sous tension et réponse READY"),
)
@htf.plug(serial=SerialPlug)
@htf.plug(power=PowerPlug)
def phase_power_on(test, serial, power):
power.enable()
response = serial.read_until("READY", timeout_s=5)
test.measurements.boot_time_ms = response.elapsed_ms
@htf.measures(
htf.Measurement("voltage_mv")
.in_range(minimum=4900, maximum=5100)
)
@htf.plug(power=PowerPlug)
def phase_voltage_check(test, power):
voltage = power.measure_voltage()
test.measurements.voltage_mv = voltage
def main():
test = htf.Test(
phase_power_on,
phase_voltage_check,
test_name="PCB Functional Test",
)
with TofuPilot(test):
test.execute(test_start=lambda: input("Entrez le numéro de série : ").strip())
if __name__ == "__main__":
main()Étape 4 : Empaqueter avec PyInstaller
PyInstaller empaquète le script et toutes les dépendances en un seul exécutable. Cela élimine les incohérences de version Python sur les stations et simplifie le déploiement à une seule copie de fichier.
PyInstaller ne peut pas détecter automatiquement les imports dynamiques d'OpenHTF, vous devez donc configurer explicitement les hooks. Le fichier spec ci-dessous gère les hidden imports connus.
# -*- mode: python ; coding: utf-8 -*-
import sys
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
block_cipher = None
# OpenHTF utilise des imports dynamiques que PyInstaller ne peut pas détecter automatiquement
openhtf_datas = collect_data_files("openhtf")
openhtf_hiddenimports = collect_submodules("openhtf")
a = Analysis(
["test_main.py"],
pathex=["."],
binaries=[],
datas=openhtf_datas + [
("plugs/", "plugs/"),
("config/", "config/"),
("VERSION", "."),
],
hiddenimports=openhtf_hiddenimports + [
"openhtf.output.callbacks",
"openhtf.output.callbacks.json_factory",
"openhtf.util.logs",
"tofupilot",
"tofupilot.openhtf",
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name="station_tests",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)Compilez l'exécutable :
#!/bin/bash
set -e
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
echo "$VERSION" > VERSION
echo "Compilation de la version $VERSION"
pyinstaller station_tests.spec --clean --noconfirm
mv dist/station_tests "dist/station_tests_v${VERSION}"
rm VERSION
echo "Compilé : dist/station_tests_v${VERSION}"Dépannage PyInstaller
| Symptôme | Cause | Correction |
|---|---|---|
ModuleNotFoundError: openhtf.util.logs | Hidden import manquant | Ajouter à hiddenimports dans le spec |
Frontend vide sur localhost:4444 | Ressources web OpenHTF non empaquetées | Vérifier collect_data_files("openhtf") dans le spec |
OSError: [Errno 2] sur un fichier de config | Fichier de données non inclus | Ajouter le répertoire config à datas dans le spec |
| Crash au deuxième lancement | Répertoire temporaire _MEIPASS résiduel | Définir runtime_tmpdir sur un chemin fixe |
Étape 5 : Configurer l'environnement de la station
Chaque station a besoin de sa propre identité et de ses identifiants. Ne codez jamais en dur les identifiants de station ou les clés API dans le script.
Variables d'environnement (Recommandé)
#!/bin/bash
# Sourcer ce fichier sur chaque station avant d'exécuter les tests
# Placer dans /etc/profile.d/tofupilot.sh pour une configuration persistante
export TOFUPILOT_API_KEY="tp_live_xxxxxxxxxxxxxxxxxxxx"
export TOFUPILOT_STATION_ID="station-floor-1-cell-3"
export STATION_SERIAL_PORT="/dev/ttyUSB0"
export STATION_BAUD_RATE="115200"Fichier de configuration de secours
Pour les stations où les variables d'environnement ne sont pas pratiques, utilisez un fichier de configuration local :
{
"api_key": "tp_live_xxxxxxxxxxxxxxxxxxxx",
"station_id": "station-floor-1-cell-3",
"serial_port": "/dev/ttyUSB0",
"baud_rate": 115200
}Chargez-le dans le script :
import json
import os
from pathlib import Path
def load_station_config() -> dict:
if os.environ.get("TOFUPILOT_API_KEY"):
return {
"api_key": os.environ["TOFUPILOT_API_KEY"],
"station_id": os.environ.get("TOFUPILOT_STATION_ID", "unknown"),
"serial_port": os.environ.get("STATION_SERIAL_PORT", "/dev/ttyUSB0"),
"baud_rate": int(os.environ.get("STATION_BAUD_RATE", "115200")),
}
config_path = Path("/etc/tofupilot/station.json")
if not config_path.exists():
raise FileNotFoundError(
f"Configuration station introuvable à {config_path}. "
"Définissez la variable d'environnement TOFUPILOT_API_KEY ou créez le fichier de configuration."
)
with config_path.open() as f:
return json.load(f)Étape 6 : Déployer sur plusieurs stations
Utilisez un script de déploiement pour pousser l'exécutable et la configuration en une seule étape. Cet exemple cible des stations Linux via SSH.
#!/bin/bash
set -e
VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
EXECUTABLE="dist/station_tests_v${VERSION}"
DEPLOY_PATH="/opt/tofupilot/station_tests"
STATIONS=(
"operator@station-01.local"
"operator@station-02.local"
"operator@station-03.local"
)
for STATION in "${STATIONS[@]}"; do
echo "Déploiement v${VERSION} vers ${STATION}..."
ssh "$STATION" "[ -f ${DEPLOY_PATH} ] && cp ${DEPLOY_PATH} ${DEPLOY_PATH}.bak || true"
scp "$EXECUTABLE" "${STATION}:${DEPLOY_PATH}"
ssh "$STATION" "chmod +x ${DEPLOY_PATH}"
ssh "$STATION" "${DEPLOY_PATH} --version 2>&1 | head -1"
echo " Terminé : ${STATION}"
done
echo "Déployé v${VERSION} sur ${#STATIONS[@]} stations."Étape 7 : Suivre les versions dans TofuPilot
TofuPilot enregistre la version logicielle à chaque exécution de test. Définissez-la explicitement pour pouvoir filtrer les exécutions par version de déploiement et corréler les variations de rendement avec les mises à jour de code.
import importlib.metadata
import openhtf as htf
from tofupilot.openhtf import TofuPilot
def get_version() -> str:
try:
return importlib.metadata.version("station-tests")
except importlib.metadata.PackageNotFoundError:
import os, sys
base = getattr(sys, "_MEIPASS", os.path.dirname(__file__))
version_file = os.path.join(base, "VERSION")
with open(version_file) as f:
return f.read().strip()
def main():
test = htf.Test(
phase_power_on,
phase_voltage_check,
test_name="PCB Functional Test",
)
with TofuPilot(test, software_version=get_version()):
test.execute(test_start=lambda: input("Entrez le numéro de série : ").strip())Comparaison des méthodes de déploiement
| Méthode | Configuration | Python requis sur la station | Retour arrière |
|---|---|---|---|
| Environnement virtuel | Faible | Oui | Remplacer le venv |
| Exécutable unique PyInstaller | Moyenne | Non | Remplacer le fichier |
| Conteneur Docker | Élevée | Non | docker pull du tag précédent |
| Partage réseau (live) | Faible | Oui | Restaurer le fichier sur le partage |
PyInstaller est le bon choix lorsque les stations ont des images OS verrouillées ou des versions Python incohérentes. Les environnements virtuels fonctionnent bien quand vous contrôlez le système d'exploitation de la station et souhaitez une itération plus rapide.