Test Station Setup

Déployer des scripts de test production

Apprenez à empaqueter, distribuer et déployer des scripts de test OpenHTF sur les stations de production avec des environnements virtuels, PyInstaller et.

JJulien Buteau
intermediate12 min de lecture14 mars 2026

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 tofupilot et openhtf disponibles 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.

setup_env.sh
#!/bin/bash
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip

Sur les stations Windows :

setup_env.bat
@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.

requirements.txt
openhtf==2.1.0
tofupilot==1.5.0
pyserial==3.5
numpy==1.26.4
pyinstaller==6.10.0

Installez et vérifiez :

install_deps.sh
#!/bin/bash
pip install -r requirements.txt
pip freeze > requirements.lock.txt  # capturer les dépendances transitives pour audit

Utilisez 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 :

pyproject.toml
[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.

test_main.py
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.

station_tests.spec
# -*- 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 :

build.sh
#!/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ômeCauseCorrection
ModuleNotFoundError: openhtf.util.logsHidden import manquantAjouter à hiddenimports dans le spec
Frontend vide sur localhost:4444Ressources web OpenHTF non empaquetéesVérifier collect_data_files("openhtf") dans le spec
OSError: [Errno 2] sur un fichier de configFichier de données non inclusAjouter le répertoire config à datas dans le spec
Crash au deuxième lancementRépertoire temporaire _MEIPASS résiduelDé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é)

station_env.sh
#!/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 :

/etc/tofupilot/station.json
{
  "api_key": "tp_live_xxxxxxxxxxxxxxxxxxxx",
  "station_id": "station-floor-1-cell-3",
  "serial_port": "/dev/ttyUSB0",
  "baud_rate": 115200
}

Chargez-le dans le script :

config/loader.py
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.

deploy.sh
#!/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.

test_main.py
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éthodeConfigurationPython requis sur la stationRetour arrière
Environnement virtuelFaibleOuiRemplacer le venv
Exécutable unique PyInstallerMoyenneNonRemplacer le fichier
Conteneur DockerÉlevéeNondocker pull du tag précédent
Partage réseau (live)FaibleOuiRestaurer 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.

Plus de guides

Mettez ce guide en pratique