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 htffrom openhtf.util import unitsfrom 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 htffrom openhtf.util import unitsfrom 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 = responsedef 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 htffrom openhtf.util import unitsfrom tofupilot.openhtf import TofuPilotMAX_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 = tempdef 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 timeimport openhtf as htffrom openhtf.plugs import BasePlugclass 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 htffrom tofupilot.openhtf import TofuPilotfrom 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 = attemptsdef 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 htffrom openhtf.plugs import BasePlugclass 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 htffrom openhtf.util import unitsfrom tofupilot.openhtf import TofuPilotfrom 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 OpenHTFdef 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 htffrom openhtf.util import unitsfrom 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 = gaindef 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