Aller au contenu

Fixtures : le générateur de cartes SD

Les cartes SD de recette (arbres LogPR<serie>.txt + PaRecPR<serie>_THLog.csv + les *.wav) pesaient autrefois plusieurs centaines de méga-octets et étaient faites à la main : impossibles à committer (binaire lourd, dépôt public et forké) et non rejouables (rien ne garantissait de les reconstruire à l'identique).

Où vivent les WAV, et pourquoi c'est à la racine

Les enregistreurs déposent leurs fichiers à la racine de la carte. Le produit accepte aussi un sous-dossier bruts/ - InspecteurDossier regarde les deux - mais c'est la racine que le parc porte.

Les seize specs produisaient pourtant un bruts/, sans qu'aucune ne puisse en décider : le champ n'existait pas. La disposition réelle n'était donc jouée par aucun cas de recette, et les parcours filmés montraient une carte que le lecteur n'a pas sous les yeux (#5281).

Depuis, brutsDansUnSousDossier tranche, et son défaut est faux : une spec qui ne dit rien décrit ce qu'un enregistreur produit. sd-prefixee est la seule à le poser à vrai, pour que la branche du sous-dossier garde sa recette - c'est la carte de quelqu'un qui a organisé ses fichiers, et le choix lui revenait naturellement.

On les décrit désormais par des specs déclaratives de quelques kilo-octets, sous recette/fixtures/spec/, qu'un générateur déterministe matérialise sur disque. La spec est la source de vérité ; l'arbre SD n'en est qu'un artefact reconstructible octet pour octet : le générateur ne tire aucune date de l'horloge ni aucun octet aléatoire.

Le générateur vit en portée test (src/test/java/fr/univ_amu/iut/recette/) : l'application distribuée n'embarque pas la fabrique de fixtures ; ce sont les tests qui fabriquent WAV/ZIP.

Le format de spec

Une spec est un fichier YAML. Tous les champs ont des valeurs par défaut raisonnables, donc une spec reste concise. Exemple commenté (le cas propre) :

fixture: sd-nominale            # nom de la carte (= nom du sous-dossier généré)
but: "Cas propre : 6 wav, un seul enregistreur, une seule nuit."

journal:                        # journal du capteur LogPR<serie>.txt
  present: true                 # false -> aucun journal (mode dégradé)
  serie: "1925492"              # n° de série déclaré (nomme aussi le fichier)
  nuit: "2026-04-22"            # date de la 1re ligne (fixe dateDebut pour l'analyseur)
  sondePresente: true           # ajoute la ligne « Sonde ... présente »
  corrompu: false               # true -> journal illisible (aucune série extractible)
  appuiSurTouche: false         # true -> « Wakeup by PINPUSH... Cpt 2 » au milieu de la nuit (#4981)
  nuitInterrompue: false        # true -> le journal s'arrête après le réveil, cycle jamais refermé (#5093)
  sessions:                     # REDÉMARRAGES supplémentaires du capteur (#3898), facultatif
    - nuit: "2026-04-25"        #   chacun repose ses paramètres après `nuit` ci-dessus
      frequenceKhz: 256         #   et peut annoncer une autre fréquence d'acquisition

thlog:                          # relevé climatique PaRecPR<serie>_THLog.csv
  present: true
  mesures: 6                    # nombre de lignes de mesures déterministes

wav:                            # paramètres communs des WAV (RIFF mono 16 bits valide)
  frequenceHz: 384000           # inscrite dans l'en-tête ; divisible par 10 (règle R10)
  dureeSecondes: 1.5            # courte, pour des fixtures légères

zip: true                       # produit aussi sd-nominale.zip (chemin de décompression)

# brutsDansUnSousDossier: true  # range les WAV dans bruts/ ; ABSENT = à la racine, ce que les
                                # enregistreurs déposent réellement (#5281)

enregistreurs:                  # un ou plusieurs enregistreurs de la carte
  - serie: "1925492"
    horodatages:                # PaRecPR<serie>_<yyyyMMdd>_<HHmmss>.wav
      - "20260422_203922"
      - "20260422_210515"
    # fauxWav: ["20260422_211500"]   # octets non-WAV : rejetés à l'import

attendu:                        # contrat de recette (voir « Le garde-fou »)
  aJournal: true
  aReleve: true
  journalLisible: true          # false -> l'inspection doit échouer
  plusieursEnregistreurs: false # bandeau « mélange »
  incoherent: false             # bandeau « incohérence »
  nuits: 1                      # nombre de nuits détectées
  etatNommage: "BRUT"           # BRUT, PREFIXE ou VIDE
  rejets: 0                     # > 0 déclenche la vérification d'import réel
  completudes: []               # une par nuit, dans l'ordre : COMPLETE, TRONQUEE ou INCONNUE

Deux variantes utiles :

  • Préfixe (carte déjà nommée en session) : un bloc prefixe: ajoute le préfixe R6 Car<carre>-<annee>-Pass<passage>-<point>- devant chaque nom de brut.

    prefixe:
      carre: "130711"
      annee: 2026
      passage: 1
      point: "Z1"
    
  • Horodatages génératifs (grosses cartes) : au lieu de lister chaque fichier, on décrit une série déterministe.

    enregistreurs:
      - serie: "1925492"
        horodatages:
          debut: "20260422_203000"
          nombre: 60
          intervalleSecondes: 300
    

Les 12 cartes de recette

Chaque carte exerce une pathologie de l'assistant d'import (voir l'étape 5 de S2 · Importer une nuit).

Carte Ce qu'elle exerce wav
sd-nominale (+ .zip) cas propre ; l'archive exerce le chemin de décompression 6
sd-melange deux enregistreurs dans le même dossier -> bandeau « mélange » 6
sd-incoherente journal et wav en désaccord (série + date) -> bandeau « incohérence » 3
sd-multi-nuits trois nuits sous un journal unique -> table des nuits 6
sd-multi-configs deux nuits, capteur reconfiguré entre les deux (384 puis 256 kHz) -> chaque nuit doit recevoir les paramètres de sa session (#3460) 4
sd-sans-journal aucun LogPR -> mode dégradé (import possible sans journal) 3
sd-journal-corrompu LogPR illisible -> l'inspection échoue avec un message clair 3
sd-prefixee bruts déjà préfixés Car... -> état de nommage PREFIXE. La seule carte rangée dans bruts/ 3
sd-rejets un faux wav parmi huit valides -> l'import aboutit, zone des rejets 9
sd-grosse soixante wav -> test de charge (progression, parallélisme, disque) 60
sd-reveil-bouton un appui sur une touche au milieu de la nuit -> une nuit, complète, et non deux (#4981) 5
sd-nuit-interrompue le journal ne se referme jamais -> nuit tronquée (#5093) 3

Régénérer les cartes

Le générateur ne fait pas partie du binaire ; on le lance en portée test via un goal de confort (non lié à une phase, jamais exécuté en CI ni au packaging) :

env -u DISPLAY ./mvnw -q test-compile exec:java@generer-sd \
  -Dexec.args="recette/fixtures/spec /chemin/vers/dest"

Chaque carte est écrite dans dest/<fixture>/ (et dest/<fixture>.zip si la spec le demande). Deux exécutions produisent des octets identiques. Passer une seule spec au lieu du dossier ne génère que cette carte.

Le garde-fou

Le bloc attendu de chaque spec est un contrat vérifié contre le code réel, pas contre une liste tenue à la main. Deux tests (esprit cliquet, sans JavaFX) amarrent le générateur à la réalité :

  1. GenerationCartesSDCliquetTest : pour chaque spec, génère la carte puis vérifie que l'inspection réelle (InspecteurDossier) constate la pathologie déclarée (mélange, incohérence, nombre de nuits, état de nommage, nombre d'originaux, journal illisible), plus le round-trip du zip via ExtracteurZip.
  2. GenerationCartesSDImportCliquetTest : pour toute spec à rejets (attendu.rejets > 0), lance un import réel headless (ServiceImport sur base SQLite jetable) et vérifie compte(REJETE). C'est le seul cas de recette qui se joue à l'import et non à la seule inspection.

Ces tests deviennent rouges si le générateur cesse de produire la bonne carte ou si un détecteur d'import change de comportement (AnalyseMelange, AnalyseCoherence, PartitionNuits, AnalyseurLogPR, ServiceImport, ExtracteurZip).

Provoquer un refus de dépôt

Les cartes SD ci-dessus fournissent des données ; elles ne permettent pas de faire refuser un dépôt par la plateforme. Or c'est le seul endroit du produit où un refus est définitif : l'archive ne repartira pas telle quelle, la reprise cesse d'être offerte, et une reconnexion réarme les refus de droits mais jamais un contenu refusé (#3687, #3689). Sans levier, ces cases ne seraient pas rejouables.

Le stub réseau des E2E CLI sert aussi l'application : ConnexionModule#urlDeBase est le seul endroit qui décide l'URL du client, et ce client est le singleton partagé par toutes les features. Pointer VIGIECHIRO_URL sur le stub suffit donc à détourner l'IHM.

# 1. Le stub, avec le statut qu'on veut voir servir sur les routes de dépôt.
VIGIECHIRO_STUB_REFUS=403 python3 src/test/bats/stub_vigiechiro.py /tmp/port /tmp/journal 5 &

# 2. L'application, pointée dessus.
VIGIECHIRO_URL="http://127.0.0.1:$(cat /tmp/port)" ./mvnw javafx:run
Statut Ce qu'il fait jouer
403 (ou 401) un refus de droits : réparable par une reconnexion, les unités se réarment
422 (ou 400) un contenu refusé : rien ne le répare, et le message ne conseille pas la reconnexion
0 ou absent aucun refus, comportement nominal

Le refus ne porte que sur /fichiers et /multipart, et c'est délibéré : refuser partout empêcherait de se connecter, donc d'atteindre le dépôt. Le cas à jouer est ce qui se passe après le refus, pas le refus lui-même.

Les cas correspondants sont le bloc A5 de la session S4.

Où ça vit

  • recette/fixtures/spec/*.yaml : les specs (source de vérité, versionnées).
  • src/test/java/fr/univ_amu/iut/recette/ : le générateur (GenerateurCartesSD, FabriqueWav, LecteurSpec, SpecCarteSd) et les deux garde-fous.
  • Le rejeu déterministe complet (générer -> importer via la CLI -> comparer à un golden) prolonge ce socle : voir la section « Rejouer une campagne » de la méthode.

La décision de conception (specs déclaratives + générateur déterministe, portée test) est consignée dans l'ADR 0015.