Test Data & Analytics

Comment stocker les résultats de test

Guide pratique du stockage des résultats de test en production : fichiers plats, schémas SQL, normes STDF et ATML, et limites de chaque approche.

JJulien Buteau
beginner10 min de lecture7 septembre 2026

Demandez comment stocker les résultats de test en production et la réponse la plus fréquente dans les forums sera qu'il n'existe pas de norme du secteur. Cette réponse est à moitié vraie. Il n'existe pas de norme unique couvrant le test fonctionnel au niveau carte comme STDF couvre le test semi-conducteur. Mais il existe des approches bien comprises, chacune avec un point de rupture connu.

Ce guide en couvre quatre, dans l'ordre où les équipes les rencontrent, et indique clairement où chacune cesse de fonctionner.

Ce qu'il faut réellement stocker

Avant de choisir un format, clarifiez les entités. Presque toutes les équipes convergent vers les mêmes cinq :

EntitéDéfinition
UnitéUn objet physique avec un numéro de série
RunUne exécution d'une procédure de test sur une unité
ÉtapeUne phase d'un run, avec son propre pass/fail
MesureUne valeur nommée avec unité et limites
ProcédureLa définition versionnée de ce qui a été testé

Les deux oubliées au début et coûteuses ensuite : la version de procédure et les relations de sous-ensembles. Si vous ne pouvez pas répondre à « quelle version du test a produit ce résultat » ou « quelle carte est entrée dans cet assemblage final », vous reconstruirez votre schéma.

Approche 1 : fichiers plats

CSV ou JSON écrits sur un partage réseau, un fichier par run.

log_to_csv.py
import csvfrom datetime import datetime, timezonedef log_run(serial, procedure, measurements, passed):    stamp = datetime.now(timezone.utc).isoformat()    path = f"//test-share/results/{serial}_{stamp}.csv"    with open(path, "w", newline="") as f:        writer = csv.writer(f)        writer.writerow(["serial", "procedure", "measurement", "value", "unit", "low", "high", "passed"])        for m in measurements:            writer.writerow([                serial, procedure, m["name"], m["value"],                m["unit"], m["low"], m["high"], m["passed"],            ])

Fonctionne jusqu'à : la première question transversale. Le rendement du mois dernier implique d'analyser des milliers de fichiers. Un changement de schéma rend les anciens fichiers incohérents. Deux stations qui écrivent simultanément finissent par corrompre quelque chose. Personne ne retrouve le fichier d'un numéro de série sans une convention de nommage qui devient elle-même un schéma.

Plafond réaliste : une station, faible volume, ou un montage vraiment temporaire.

Approche 2 : un schéma SQL maison

L'étape suivante naturelle, et la bonne réponse pour beaucoup d'équipes.

schema.sql
CREATE TABLE unit (    id            BIGSERIAL PRIMARY KEY,    serial_number TEXT NOT NULL UNIQUE,    part_number   TEXT NOT NULL,    created_at    TIMESTAMPTZ NOT NULL DEFAULT now());CREATE TABLE run (    id                BIGSERIAL PRIMARY KEY,    unit_id           BIGINT NOT NULL REFERENCES unit(id),    procedure_id      TEXT NOT NULL,    procedure_version TEXT NOT NULL,    station_id        TEXT NOT NULL,    started_at        TIMESTAMPTZ NOT NULL,    duration_s        NUMERIC NOT NULL,    passed            BOOLEAN NOT NULL);CREATE TABLE measurement (    id          BIGSERIAL PRIMARY KEY,    run_id      BIGINT NOT NULL REFERENCES run(id) ON DELETE CASCADE,    step_name   TEXT NOT NULL,    name        TEXT NOT NULL,    value       DOUBLE PRECISION,    unit        TEXT,    lower_limit DOUBLE PRECISION,    upper_limit DOUBLE PRECISION,    passed      BOOLEAN NOT NULL);CREATE INDEX ON run (procedure_id, started_at DESC);CREATE INDEX ON measurement (run_id);CREATE INDEX ON measurement (name, run_id);

Le rendement au premier passage devient alors une requête plutôt qu'un script :

first_pass_yield.sql
SELECT    date_trunc('day', first_run.started_at) AS day,    count(*) FILTER (WHERE first_run.passed) * 100.0 / count(*) AS fpy_percent,    count(*) AS units_testedFROM (    SELECT DISTINCT ON (unit_id)        unit_id, started_at, passed    FROM run    WHERE procedure_id = 'FCT-001'    ORDER BY unit_id, started_at ASC) AS first_runGROUP BY dayORDER BY day DESC;

Notez le DISTINCT ON et le tri ascendant. Le rendement au premier passage désigne le premier run par unité, pas n'importe quel run réussi. Se tromper là-dessus est le bug de reporting le plus courant des systèmes maison, et il gonfle toujours le chiffre.

Fonctionne jusqu'à : ce que le retard d'analyses croisse plus vite que vous ne l'absorbez. Cpk, cartes de contrôle avec limites correctes, Pareto des modes de défaillance, détection de dérive, comparaison par station et rapports opérateurs sont chacun un petit projet. Puis quelqu'un demande les mêmes chiffres sur trois sites à connectivité intermittente.

Plafond réaliste : viable longtemps si quelqu'un en assume la charge. Le coût est de l'ingénierie continue, pas de la licence.

Approche 3 : normes du secteur

Deux normes comptent, aucune ne couvre tout.

STDF (Standard Test Data Format) est la norme semi-conducteur de Teradyne, largement supportée par les ATE. Binaire, compacte, elle suppose wafers, dies et bins. Si vous testez du silicium, utilisez-la. Si vous testez des cartes assemblées, son modèle ne convient pas et le forcer coûte plus qu'il ne rapporte.

ATML (Automatic Test Markup Language) est la famille IEEE 1671, en XML, conçue pour la description et l'échange de résultats, surtout en aéronautique et défense. Complète et verbeuse. Surtout utile quand un contrat l'exige.

Il y a aussi WSJF et WSXF, spécifiques à WATS plutôt que normes ouvertes, à connaître surtout parce que migrer depuis WATS implique de les traiter.

Résumé honnête : pour le test fonctionnel au niveau carte, aucune norme n'a atteint l'adoption de STDF en semi-conducteur. La plupart des équipes finissent avec un schéma propriétaire, le leur ou celui d'un éditeur.

Approche 4 : une plateforme de données de test

Utiliser un système conçu pour ça, où le schéma et les analyses viennent avec.

functional_test.py
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("rail_3v3")    .in_range(3.2, 3.4)    .with_units(units.VOLT),    htf.Measurement("current_draw")    .in_range(0.1, 0.5)    .with_units(units.AMPERE),)def power_rail_test(test):    test.measurements.rail_3v3 = 3.31    test.measurements.current_draw = 0.24def main():    test = htf.Test(        power_rail_test,        procedure_id="FCT-001",        part_number="PCBA-200",    )    test.add_output_callbacks(upload())    test.execute(test_start=lambda: input("Scanner le numéro de série : "))if __name__ == "__main__":    main()

La définition de mesure porte le nom, les limites et les unités : le stockage et l'évaluation pass/fail découlent du code de test au lieu d'être maintenus séparément. Rendement, Cpk, cartes de contrôle et Pareto sont calculés plutôt qu'interrogés.

Les options de cette catégorie incluent WATS, l'acteur historique, et TofuPilot. Les deux gèrent les entités ci-dessus. Ils diffèrent surtout par le modèle de déploiement, la forme de la licence, et le fait que le code de test vive ou non dans votre dépôt Git.

Fonctionne jusqu'à : un besoin réellement hors du modèle du produit. À ce moment vous revenez à l'approche 2, idéalement avec l'export de l'éditeur.

Choisir

SituationApproche
Une station, volume prototypeFichiers plats, mais préparez la sortie
Plusieurs stations, un ingénieur pour en assumer la chargeSchéma SQL
Test semi-conducteurSTDF
Format imposé par contratATML ou ce que le contrat exige
Test au niveau carte, analyses souhaitées sans les construirePlateforme de données de test

Les erreurs les plus coûteuses

Ne pas versionner la procédure. Quand les limites changent, les anciens résultats deviennent ininterprétables si vous n'avez pas enregistré quelle version les a produits.

Stocker le pass/fail sans la valeur. Un booléen dit qu'une unité a échoué. La valeur mesurée dit qu'elle dérivait depuis trois semaines.

Ignorer les sous-ensembles. La traçabilité du numéro de série de l'assemblage final jusqu'aux cartes se rajoute à grands frais. Modélisez-la dès le premier jour même sans l'utiliser.

Calculer le rendement sur n'importe quel run réussi. Le rendement au premier passage, c'est le premier run par unité. Compter les retests le gonfle, parfois fortement.

Aucune discipline de fuseau horaire. Stockez en UTC. Sinon trois sites dans trois fuseaux ne s'accorderont pas sur le jour d'une défaillance.

Plus de guides

Mettez ce guide en pratique