Test Station Setup

Déployer des tests sur les stations via Git

Poussez sur une branche et chaque station liée exécute la nouvelle procédure, avec artefacts immuables et rollback instantané. Plus la méthode manuelle.

JJulien Buteau
intermediate12 min de lecture14 mars 2026

Amener un script de test sur les stations de production de façon fiable est l'un des problèmes les moins discutés du test manufacturier. Les approches courantes, copie SSH, partage réseau, clé USB, partagent la même faiblesse : personne ne peut dire avec certitude quelle station exécute quelle version.

Le conseil vers lequel les ingénieurs convergent est de garder une configuration stable dans Git et de faire en sorte que les stations la récupèrent. Cet instinct est juste, et c'est ce que le modèle ci-dessous automatise.

Pourquoi le déploiement manuel échoue

Les modes de défaillance sont constants :

  • Dérive de versions. La station 3 était hors ligne au dernier déploiement et tourne encore avec les limites du mois dernier. Personne ne le remarque avant que son rendement diverge.
  • Déploiements partiels. Une boucle de copie échoue à mi-parcours. Certaines stations sont à jour, d'autres non, et le script ne dit pas lesquelles.
  • Pas de rollback. Une mauvaise version atteint toutes les stations en même temps et revenir en arrière signifie refaire tout le processus sous pression.
  • Modifications locales non suivies. Quelqu'un a corrigé quelque chose directement sur une station. Ce correctif n'existe nulle part ailleurs et disparaît au déploiement suivant.
  • Version de Python différente. Le script marche sur le poste de développement et échoue sur une station.

Le problème de fond : l'artefact déployé n'est pas identifiable. Si vous ne pouvez pas nommer exactement ce qui tourne, vous ne pouvez pas raisonner dessus.

Déployer depuis un push Git

Connectez un dépôt à une procédure, et chaque push sur une branche suivie construit un déploiement. Les pushes sur la branche de production se déploient sur toutes les stations liées ; les autres branches produisent des déploiements de préversion que vous pouvez épingler à une seule station.

GitHub, GitLab et Bitbucket Data Center sont supportés.

Une procédure est un fichier .yaml qui déclare la séquence, les mesures et leurs limites. Les phases sont de simples fonctions Python, et les plugs connectent les instruments sous forme de classes Python.

procedure.yaml
23 lines
name: Test fonctionnel PCBAunit:  serial_number:    default_value: "PCBA000001"  part_number:    default_value: "PCBA-200"plugs:  - name: Multimeter    python: plugs.dmm:Multimetermain:  - name: Power Rail Test    python: phases.power_rail    measurements:      - name: Rail 3V3        unit: V        validators:          - operator: ">="            expected_value: 3.2          - operator: "<="            expected_value: 3.4
phases/power_rail.py
def power_rail(measurements, multimeter):    measurements.rail_3v3 = multimeter.read_voltage()

Les limites vivant dans le YAML plutôt que dans le Python, resserrer une limite est un diff d'une ligne qui se relit proprement et reste visible dans l'historique des déploiements.

Commitez, poussez, les stations récupèrent :

deploy.sh
git add procedure.yaml phases/power_rail.pygit commit -m "Resserrer les limites du rail 3V3 à 3,2-3,4 V"git push origin main

Le build produit un artefact immuable portant l'exécutable, le runtime résolu et tout ce dont la station a besoin. Une station exécutant dep_abc123 exécute toujours exactement ce code.

Déploiement progressif

Pour valider un changement avant qu'il n'atteigne tout l'atelier, épinglez une préversion à une station :

preview.sh
git checkout -b limites-resserreesgit push origin limites-resserrees# Épinglez le déploiement de préversion à une station depuis le dashboard

Faites tourner une équipe sur cette station, comparez le rendement aux autres, puis fusionnez vers production.

Revenir en arrière

Les artefacts étant immuables et conservés, le rollback consiste à sélectionner un déploiement précédent plutôt qu'à reconstruire. La station le récupère à sa prochaine vérification.

Autres façons de créer un déploiement

  • Déclenchement manuel depuis la page Déploiements, par SHA de commit ou branche.
  • CLI, qui construit depuis votre copie de travail locale, avec ou sans dépôt connecté.

Déployer sans la plateforme

Si vous n'utilisez pas le déploiement TofuPilot, les mêmes principes s'appliquent et vous pouvez les construire vous-même.

Fixez vos dépendances

Les dépendances non fixées sont la cause la plus courante des échecs « ça marche sur mon poste ».

requirements.txt
openhtf==1.6.1tofupilot[openhtf]==2.16.0pyserial==3.5numpy==1.26.4pyinstaller==6.10.0
install_deps.sh
#!/bin/bashpip install -r requirements.txtpip freeze > requirements.lock.txt  # dépendances transitives pour audit

Empaquetez en un exécutable unique

PyInstaller réunit le script et ses dépendances en un fichier, ce qui retire la version Python de la station de l'équation.

OpenHTF utilise des imports dynamiques que PyInstaller ne détecte pas, d'où les hidden imports explicites :

station_tests.spec
48 lines
# -*- mode: python ; coding: utf-8 -*-from PyInstaller.utils.hooks import collect_data_files, collect_submodulesblock_cipher = Noneopenhtf_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=[],    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,    strip=False,    upx=False,    runtime_tmpdir=None,    console=True,)

Dépannage PyInstaller :

SymptômeCauseCorrection
ModuleNotFoundError: openhtf.util.logsHidden import manquantAjouter à hiddenimports
Frontend vide sur localhost:4444Ressources web non empaquetéesVérifier collect_data_files("openhtf")
OSError: [Errno 2] sur la configFichier non inclusAjouter le répertoire à datas
Crash au deuxième lancement_MEIPASS résiduelDéfinir runtime_tmpdir

Gardez l'identité de station hors du code

station_env.sh
#!/bin/bash# Placer dans /etc/profile.d/tofupilot.shexport 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"
config/loader.py
22 lines
import jsonimport osfrom pathlib import Pathdef 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}."        )    with config_path.open() as f:        return json.load(f)

Poussez vers les stations

deploy_manual.sh
21 lines
#!/bin/bashset -eVERSION=$(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

Notez la copie .bak avant écrasement. C'est votre rollback, et il ne remonte que d'une version.

Enregistrez la version à chaque run

Quelle que soit la méthode, enregistrez quelle version a produit chaque résultat. Sans cela, impossible de corréler un changement de rendement avec une release.

La version est une propriété du run : enregistrez-la là où vivent vos autres métadonnées. Avec OpenHTF, les métadonnées du test record la portent :

test_main.py
29 lines
import importlib.metadataimport openhtf as htffrom tofupilot.openhtf import uploaddef 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__))        with open(os.path.join(base, "VERSION")) as f:            return f.read().strip()def record_version(test):    test.metadata["software_version"] = get_version()def main():    test = htf.Test(        record_version,        phase_power_on,        phase_voltage_check,        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",        part_number="PCBA-200",    )    test.add_output_callbacks(upload())    test.execute(lambda: input("Numéro de série : ").strip())

Avec le déploiement Git, vous l'obtenez gratuitement : l'identifiant de déploiement nomme déjà l'artefact exact, il n'y a rien à enregistrer à la main.

Comparer les options

MéthodeMise en placePython requisRollbackCertitude de version
Déploiement GitConnecter un dépôtNonSélectionner un déploiementExacte, par station
PyInstaller via SSHMoyenneNonRestaurer .bak, une versionSuivi manuel
Environnement virtuelFaibleOuiRemplacer le venvSuivi manuel
Conteneur DockerÉlevéeNondocker pull tag précédentExacte, par tag
Partage réseauFaibleOuiRestaurer le fichierAucune

La ligne partage réseau est à éviter. Elle paraît la plus simple et c'est la seule méthode où une station peut récupérer silencieusement un fichier à moitié écrit en pleine équipe.

À vérifier après tout déploiement

  1. Chaque station rapporte la version attendue. Sinon, elle n'a pas été mise à jour.
  2. Les premiers runs après déploiement passent. Un déploiement cassé se voit surtout sur les premières unités.
  3. Le rendement après correspond au rendement avant. Si les limites ont changé volontairement, attendez-vous à une marche ; sinon, une marche signale un problème.

Le déploiement est l'un des rares endroits du test de production où pouvoir annuler vite compte plus que réussir du premier coup.

Plus de guides

Mettez ce guide en pratique