Getting Started

Gérer les échecs de test et retentatives

Apprenez à contrôler le comportement en cas d'échec des phases OpenHTF, implémenter une logique de nouvelle tentative et garantir un teardown propre dans.

JJulien Buteau
intermediate10 min de lecture14 mars 2026

Quand une mesure échoue dans OpenHTF, le comportement par défaut est de marquer la phase comme échouée et de continuer l'exécution des phases suivantes. En production, vous avez besoin d'un contrôle délibéré : quand arrêter prématurément, quand retenter, et comment préserver les données d'échec dans TofuPilot pour l'analyse.

Prérequis

  • Client Python TofuPilot installé : pip install tofupilot
  • OpenHTF installé : pip install openhtf
  • Familiarité de base avec les phases et mesures OpenHTF

Comportement par défaut en cas d'échec de phase

Les phases OpenHTF retournent un PhaseResult. Quand une mesure échoue, la phase est marquée FAIL mais l'exécution continue sauf si vous l'arrêtez explicitement.

tests/voltage_check.py
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot

@htf.measures(
    htf.Measurement('rail_3v3_voltage').in_range(3.2, 3.4).with_units(units.VOLT),
    htf.Measurement('rail_5v_voltage').in_range(4.85, 5.15).with_units(units.VOLT),
)
def check_power_rails(test):
    test.measurements.rail_3v3_voltage = 3.1  # échoue : en dessous du minimum
    test.measurements.rail_5v_voltage = 5.02  # réussit

@htf.measures(
    htf.Measurement('uart_echo').equals('OK'),
)
def check_uart(test):
    # Cette phase s'exécute même si check_power_rails a échoué
    test.measurements.uart_echo = 'OK'

def main():
    test = htf.Test(check_power_rails, check_uart)
    with TofuPilot(test):
        test.execute(test_start=lambda: 'SN-001')

Les deux phases s'exécutent. L'exécution est envoyée à TofuPilot comme FAIL avec la mesure défaillante spécifique enregistrée.

Arrêt au premier échec

Pour les tests matériels où un rail d'alimentation défaillant rend les tests suivants sans objet, arrêtez prématurément avec PhaseResult.STOP.

tests/production_sequence.py
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot

@htf.measures(
    htf.Measurement('rail_3v3_voltage').in_range(3.2, 3.4).with_units(units.VOLT),
)
def check_power_rails(test):
    test.measurements.rail_3v3_voltage = read_adc_channel(0)

    if not test.measurements.rail_3v3_voltage.is_pass:
        test.logger.error('Rail d\'alimentation hors spécification, abandon de la séquence')
        return htf.PhaseResult.STOP

@htf.measures(
    htf.Measurement('uart_echo').equals('OK'),
)
def check_uart(test):
    response = send_uart_command('PING')
    test.measurements.uart_echo = response

def main():
    test = htf.Test(check_power_rails, check_uart)
    with TofuPilot(test):
        test.execute(test_start=lambda: 'SN-001')

Quand check_power_rails retourne PhaseResult.STOP, check_uart est ignorée. TofuPilot enregistre l'exécution comme FAIL avec les phases check_uart marquées comme non exécutées.

Retenter une phase en cas d'échec

Les nouvelles tentatives sont utiles pour les échecs transitoires : timeouts de communication, tensions en stabilisation ou contacts intermittents. Utilisez PhaseResult.REPEAT avec un compteur pour éviter les boucles infinies.

tests/comms_test.py
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot

MAX_RETRIES = 3

@htf.measures(
    htf.Measurement('i2c_device_present').equals(True),
)
def check_i2c_device(test, retries=[0]):
    found = probe_i2c_address(0x48)
    test.measurements.i2c_device_present = found

    if not found and retries[0] < MAX_RETRIES:
        retries[0] += 1
        test.logger.warning('Périphérique I2C non trouvé, tentative %d/%d', retries[0], MAX_RETRIES)
        return htf.PhaseResult.REPEAT

    retries[0] = 0  # réinitialiser pour les prochaines exécutions

@htf.measures(
    htf.Measurement('i2c_temperature').in_range(-10, 85).with_units(units.DEGREE_CELSIUS),
)
def read_i2c_temperature(test):
    temp = read_temperature_sensor(0x48)
    test.measurements.i2c_temperature = temp

def main():
    test = htf.Test(check_i2c_device, read_i2c_temperature)
    with TofuPilot(test):
        test.execute(test_start=lambda: 'SN-002')

L'utilisation d'un argument par défaut mutable (retries=[0]) permet de persister le compteur entre les appels REPEAT au sein de la même exécution de phase. Réinitialisez-le avant de retourner pour éviter un état obsolète sur le prochain DUT.

Nouvelle tentative avec backoff via un plug

Pour les lignes de production avec plusieurs montages, un plug permet de réutiliser la logique de nouvelle tentative et de garder le code des phases propre.

plugs/comms_plug.py
import time
import openhtf as htf
from openhtf.plugs import BasePlug

class CommPlug(BasePlug):
    def setUp(self):
        import serial
        self._port = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)

    def send_with_retry(self, command, expected, attempts=3, delay=0.5):
        response = ''
        for attempt in range(attempts):
            self._port.write(f'{command}
'.encode())
            response = self._port.readline().decode().strip()
            if response == expected:
                return response, attempt + 1
            time.sleep(delay)
        return response, attempts

    def tearDown(self):
        if self._port:
            self._port.close()
tests/serial_test.py
import openhtf as htf
from tofupilot.openhtf import TofuPilot
from plugs.comms_plug import CommPlug

@htf.plug(comm=CommPlug)
@htf.measures(
    htf.Measurement('firmware_version').matches_regex(r'v\d+\.\d+\.\d+'),
    htf.Measurement('version_retry_count').in_range(minimum=1),
)
def check_firmware_version(test, comm):
    response, attempts = comm.send_with_retry('VERSION', expected='v2.1.0', attempts=3)
    test.measurements.firmware_version = response
    test.measurements.version_retry_count = attempts

def main():
    test = htf.Test(check_firmware_version)
    with TofuPilot(test):
        test.execute(test_start=lambda: 'SN-003')

Enregistrer version_retry_count comme mesure permet à TofuPilot de mettre en évidence la fréquence des nouvelles tentatives sur l'ensemble de votre parc de production. Un pic de tentatives sur une station spécifique indique un problème de montage avant qu'il ne devienne un problème de rendement.

Teardown propre après les échecs

Les plugs avec une méthode tearDown sont toujours appelés par OpenHTF, même quand une phase échoue ou que le test s'arrête prématurément. Utilisez cela pour libérer les ressources matérielles de manière fiable.

plugs/fixture_plug.py
import openhtf as htf
from openhtf.plugs import BasePlug

class FixturePlug(BasePlug):
    def setUp(self):
        self._power_on = False
        self._relay_closed = False

    def power_on_dut(self):
        enable_power_rail()
        self._power_on = True

    def close_test_relay(self):
        close_relay(1)
        self._relay_closed = True

    def tearDown(self):
        # S'exécute toujours, même en cas de STOP ou d'exception
        if self._relay_closed:
            open_relay(1)
        if self._power_on:
            disable_power_rail()
tests/powered_test.py
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
from plugs.fixture_plug import FixturePlug

@htf.plug(fixture=FixturePlug)
@htf.measures(
    htf.Measurement('leakage_current').in_range(maximum=50).with_units(units.AMPERE),
)
def measure_leakage(test, fixture):
    fixture.power_on_dut()
    fixture.close_test_relay()

    current_ua = read_current_meter() * 1e6
    test.measurements.leakage_current = current_ua

    if not test.measurements.leakage_current.is_pass:
        test.logger.error('Courant de fuite %.1f uA dépasse la limite de 50 uA', current_ua)
        return htf.PhaseResult.STOP
    # fixture.tearDown() appelé automatiquement par OpenHTF

def main():
    test = htf.Test(measure_leakage)
    with TofuPilot(test):
        test.execute(test_start=lambda: 'SN-004')

Comparaison des stratégies de gestion des échecs

StratégieQuand l'utiliserPhaseResultPhases suivantes
Continuer en cas d'échecLes échecs sont indépendants, collecter toutes les données(par défaut)Exécutées
Arrêt au premier échecLes tests suivants sont invalides après un échecSTOPIgnorées
Retenter en cas d'erreur transitoireL'échec peut être un problème de montageREPEATExécutées après la tentative
Retenter avec limiteIdem, mais avec un nombre maximum de tentativesREPEAT + compteurExécutées ou STOP

Enregistrement des échecs pour l'analyse

TofuPilot capture automatiquement chaque mesure échouée. Pour rendre les échecs exploitables en analyse, ajoutez du contexte structuré avec test.logger.

tests/full_sequence.py
import openhtf as htf
from openhtf.util import units
from tofupilot.openhtf import TofuPilot

@htf.measures(
    htf.Measurement('oscillator_freq_hz').in_range(7_990_000, 8_010_000).with_units(units.HERTZ),
)
def check_oscillator(test):
    freq = measure_frequency(channel=2)
    test.measurements.oscillator_freq_hz = freq

    if not test.measurements.oscillator_freq_hz.is_pass:
        deviation_ppm = abs(freq - 8_000_000) / 8_000_000 * 1e6
        test.logger.error(
            'Oscillateur %.0f Hz (%.1f ppm de 8 MHz)',
            freq, deviation_ppm,
        )

@htf.measures(
    htf.Measurement('adc_offset_mv').in_range(-5, 5),
    htf.Measurement('adc_gain_error_pct').within_percent(1.0, 0.1),
)
def calibrate_adc(test):
    offset = measure_adc_offset()
    gain = measure_adc_gain()

    test.measurements.adc_offset_mv = offset * 1000
    test.measurements.adc_gain_error_pct = gain

def main():
    test = htf.Test(check_oscillator, calibrate_adc, name='PCB-Rev-C')
    with TofuPilot(test):
        test.execute(test_start=lambda: 'SN-005')

Les messages de log apparaissent dans la vue détaillée de l'exécution TofuPilot aux côtés de la mesure défaillante. Le contexte quantitatif (déviation en ppm, pas seulement réussite/échec) accélère l'analyse des causes racines lors de l'examen des échecs sur un lot.

Plus de guides

Mettez ce guide en pratique