Skip to content

Compatibilité PHP 5.6 → 8.5 + suite de tests de non-régression - #6

Open
mhoareau wants to merge 11 commits into
AssociationInfoclimat:masterfrom
MeteoR-OI:fix/php8-compat
Open

Compatibilité PHP 5.6 → 8.5 + suite de tests de non-régression#6
mhoareau wants to merge 11 commits into
AssociationInfoclimat:masterfrom
MeteoR-OI:fix/php8-compat

Conversation

@mhoareau

Copy link
Copy Markdown

Compatibilité PHP 5.6 → 8.5 + suite de tests de non-régression (sans casser les stations existantes)

Le script static.php n'avait plus évolué depuis 2020 (PHP 7.x) et présentait plusieurs
incompatibilités avec PHP 8. Comme il tourne sur de nombreuses stations encore installées
sur d'anciennes versions de PHP, l'objectif de cette PR a été de le rendre compatible du legacy
au plus récent, sans jamais changer le comportement sur les versions où il fonctionnait déjà
.

Pour garantir ce « sans régression », le travail a suivi une méthode de reprise de code hérité en
trois étapes, chacune vérifiable indépendamment. Rien n'a été modifié « à l'aveugle ».


Étape 1 — Mettre le comportement actuel sous test (aucune modification de code)

Ajout d'une suite de non-régression « golden master » + une CI, sans toucher à
static.php ni config.php :

  • Lanceur de caractérisation en PHP pur, sans Composer, portable sur toute la plage 5.6 → 8.5.
    Il exécute le script contre des bases SQLite contrôlées (fixtures figées), horloge gelée
    (libfaketime, car le script lit time()/date()), et compare stdout + StatIC_<id>.txt +
    code de sortie
    à des snapshots approuvés. 8 scénarios : relevé récent / ancien (> 20 min),
    unités US / METRIC / METRICWX, valeurs nulles, vent calme, unité inconnue, base vide.
  • .github/workflows/ci.yml : matrice PHP 5.6 → 8.5 (shivammathur/setup-php) + php -l.

Constat documenté (code inchangé) : identique de 5.6 à 7.4 ; sur 8.0+, deux divergences
observables — la direction du vent par vent calme et le rendu de date sur base vide — corrigées
à l'étape 3
et prouvées par ces mêmes snapshots.

Étape 2 — Rendre le script testable (refactor minimal, comportement inchangé)

  • static.php devient incluable (garde CLI PHP_SAPI==='cli' && realpath($argv[0])===__FILE__) ;
    les I/O (horloge, FTP, base, écriture de fichier) sont injectables pour être testées sans
    serveur réel.
  • La logique dupliquée entre les branches SQLite et MySQL (conversions d'unités, moyenne des angles)
    est factorisée en fonctions communes.
  • Couverture 100 % lignes + branches (PHPUnit + Xdebug sur PHP 7.4 ; le chemin MySQL est couvert
    via un service MariaDB en CI), seuil imposé dans la CI.

Étape 3 — Compatibilité PHP 5.6 → 8.5 (sans régression legacy)

Chaque correctif est borné à la compatibilité ; les snapshots de l'étape 1 garantissent qu'aucun
comportement observable ne change sur les anciennes versions (matrice CI entièrement verte 5.6 → 8.5).
Tous les correctifs sont 5.6-safe :

  • Direction du vent par vent calme : if ($avg_wind_10 != 0)if ($avg_wind_10 !== '' && $avg_wind_10 != 0).
    Conserve l'intention de V2.5 (« pas de direction si le vent moyen est nul/absent ») sur PHP 8, où
    '' != 0 vaut désormais true.
  • getopt(null, …)getopt('', …) : passer null au 1er paramètre est déprécié en 8.1+.
  • Garde --debug absent : isset($options['debug']) ? … : '' (évite « undefined array key » en 8.0+).
  • Fuseau horaire positionné avant le premier date() (supprime un warning quand date.timezone
    n'est pas défini dans php.ini).
  • Base vide : gmdate((int)$stop, …)gmdate(null) vaut « maintenant » en PHP 8 vs epoch 0 en
    7.x ; on force un rendu cohérent, identique au legacy.

Ajouts additifs (rétro-compatibles)

  • Option --config=<chemin> (défaut ./config.php, comportement inchangé sans l'option) ;
    config.php est désormais ignoré par git avec un modèle config.example.php — les réglages
    ne sont plus écrasés à la mise à jour, et l'outil peut charger sa config depuis un chemin arbitraire.
  • Constante lisible STATIC_VERSION ; la ligne version= du .txt en dérive désormais
    (weewx-<db>-<major.minor>) au lieu d'un littéral codé en dur — une seule source de vérité,
    plus de désynchronisation entre la version de l'outil et le tag du fichier. (La valeur exacte
    reste votre choix : c'est STATIC_VERSION qui la pilote.)

Extensions

ext-mysqli et ext-ftp restent requises. ext-ftp n'est pas retirée de PHP jusqu'en 8.5
(elle est simplement absente de certaines images Docker par défaut) : aucune bascule vers cURL
n'est nécessaire.


Comment vérifier

# lint + caractérisation sur une version donnée (ex. via Docker)
docker run --rm -v "$PWD":/app -w /app php:8.5-cli sh -c \
  'apt-get update && apt-get install -y faketime >/dev/null && php -l static.php && php tests/characterize.php'

La CI rejoue automatiquement la matrice 5.6 → 8.5 + la porte de couverture 100 %.
Le runtime reste deux fichiers PHP purs (static.php + config.example.php), sans aucune
dépendance à l'exécution — PHPUnit est un outil de développement uniquement.


Merci de votre relecture — je reste à disposition pour ajuster (découper en PR plus petites,
renommer la version, etc.).

Mickaël Hoareau added 11 commits July 18, 2026 09:28
Golden-master suite pinning the current observable behaviour of static.php so
later PHP-compat work can be proven regression-free. Pure PHP, no Composer, no
runtime deps; runs identically on PHP 5.6 -> 8.5.

- Frozen wall clock (libfaketime) since the script reads time()/date().
- Fixed, prebuilt SQLite `archive` fixtures = byte-identical golden input for
  every PHP version (never rebuilt by the version under test).
- Captures stdout + generated StatIC_<id>.txt + exit code vs approved snapshots;
  warnings/deprecations routed to stderr as informational (non-gating).
- 6 scenarios: recent/stale, US/METRIC/METRICWX units, null sensors, and the
  calm-wind case that exposes the '' != 0 PHP7<->PHP8 change.

Production code (static.php, config.php) is NOT modified (Phase 1 = characterize).
GitHub Actions workflow running the characterization suite across the whole
target range via shivammathur/setup-php. Versions where the legacy code already
diverges (8.0-8.5) are continue-on-error so the build stays globally green: the
red is a documented baseline finding, not a regression to fix in Phase 1.

Baseline: 5.6-7.4 fully pass; 8.0+ diverge only on the calm-wind direction case
('' != 0); 8.1+ additionally emit a getopt(null) deprecation (non-gating).
Move the whole script into functions run only via a CLI guard
(`PHP_SAPI==='cli' && realpath($argv[0])===__FILE__`), so it can be included and
unit-tested without executing main. Behaviour is preserved BYTE-FOR-BYTE: the
characterization snapshots stay green on the whole legacy range (5.6-7.4).

- Factor the unit-conversion logic (duplicated identically across the sqlite/mysql
  branches) into small pure functions, covered once.
- Introduce seams: injectable clock (now), FTP client (Static_Ftp interface +
  Static_FtpExt real adapter + fakeable), DB handle, and file output; die()-on-FTP-
  failure becomes a signalled return with the SAME stdout and exit code.
- Preserve the legacy quirks verbatim (not "fixed" here): gmdate (sqlite) vs date
  (mysql), and the SUM(rain) query using the table name without the db_name prefix.
- Add --config=<path> (default ./config.php, legacy unchanged) and a readable
  STATIC_VERSION marker (does not change the .txt output).
- No runtime dependency added: still two pure PHP files.

Stays 5.6-safe; PHP 8.x compatibility is deliberately NOT addressed here (Phase 3).
Rename the tracked config.php to config.example.php and gitignore config.php, so an
installer/user drops their own config.php (or points --config=<path> elsewhere)
without dirtying the clone and without risking a real config being committed. The
default path stays ./config.php, so existing setups are unaffected.
Reach 100% line AND branch coverage of static.php on the pinned legacy PHP (7.4,
Xdebug), driving static_run() through the seams:
- converters/report/wind-dir/upload/config covered by unit tests (FakeFtp for the
  FTP branches: success + connect/login failure, no server);
- the sqlite path via the committed golden fixtures + injected clock, asserting the
  produced StatIC_<id>.txt equals the approved snapshot and that FTP fires iff fresh;
- the mysql path against a real MariaDB (env-driven; skipped when absent);
- two new scenarios, unit_unknown (converter fallthroughs) and empty_db (degenerate
  null propagation), added to the golden-master suite too.
Paths are not gated (combinatorial on the sequential collectors); the thin
Static_FtpExt adapter is @codeCoverageIgnore (its logic is covered via the fake).
coverage_gate.php enforces 100% lines+branches. PHPUnit is dev-only; the shipped
script keeps zero runtime dependencies.
…riaDB)

New `coverage` job runs PHPUnit with a MariaDB service and enforces the 100%
line+branch gate via coverage_gate.php, alongside the existing 5.6-8.5 matrix.
Make the whole matrix (PHP 5.6 -> 8.5) produce byte-identical output, without any
observable change on the legacy range (the characterization snapshots are unchanged).

- Wind direction on calm wind: `$avg_wind_10 != 0` -> `$avg_wind_10 !== '' && != 0`.
  Keeps the V2.5 intent ("no direction when mean wind is 0/absent") on PHP 8, where
  '' != 0 is now true (it was false in 7.x).
- getopt(null, ...) -> getopt('', ...): passing null to arg AssociationInfoclimat#1 is deprecated in 8.1+.
- Guard a missing --debug: isset(...) ? ... : '' (avoids "undefined array key" in 8.0+).
- Set the timezone before the first date() call (removes the warning emitted when
  date.timezone is not set in php.ini).
- Empty database: gmdate((int)$stop, ...) -- gmdate(null) means "now" in PHP 8 vs epoch
  0 in 7.x; forcing int keeps the legacy rendering (01/01/1970) consistent across versions.

Legacy quirks are still preserved verbatim (gmdate vs date per backend, the un-prefixed
SUM(rain) query). Stays 5.6-safe; 100% line+branch coverage maintained.
With the Phase 3 compatibility fixes every version now matches the approved snapshots,
so the characterization matrix is fully required -- remove continue-on-error.
The Lint step still ran `php -l config.php`, but config.php was renamed to
config.example.php (and gitignored) in Phase 2, so on a clean checkout the file is
absent -> "Could not open input file: config.php" -> exit 1 -> under `bash -e` the
Characterization step is skipped and the whole matrix goes red. Lint the tracked
template instead. Workflow-only fix; no runtime file changes.
Document the PHP 5.6-8.5 compatibility work, the dependency-free test suite + CI
matrix with 100% line/branch coverage, the new --config option with the
config.example.php template, and the STATIC_VERSION marker.

Appended after V2.5 to keep the changelog's existing chronological order
(oldest first), matching the upstream convention.
The version= tag of the produced StatIC .txt was hard-coded to
"weewx-<db_type>-2.5", frozen during the PHP 8 compatibility work while
the tool moved to 2.6.0. Derive it from STATIC_VERSION in major.minor so
it now reads "weewx-<db_type>-2.6" and can no longer drift from the tool
version (single source of truth).

Weather data and calculations are unchanged: the only snapshot delta is
the version= line (and the matching "Version script :" debug line, same
string). Unit/integration tests now derive the expected version from
STATIC_VERSION too. README changelog nuanced accordingly.
@RaphaelChochon

Copy link
Copy Markdown
Collaborator

Je n'ai plus le temps ni les capacités de dépoussiérer ce projet et vérifier les modifs, mais la description semble clair, merci pour ce travail !
Ne faisant plus partie de l'association, je laisse peut-être @jlecordier regarder ça pour éventuellement merger cette PR ?

Comment thread static.php
Comment on lines -23 to -24
// VERSION - NE PAS MODIFIER !!
$version = "weewx-".$db_type."-2.5";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C'est écrit NE PAS MODIFIER, pourquoi est-ce que c'est modifié ? (Je ne sais pas pourquoi ça ne doit pas être modifié)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jlecordier c'est devenu une constante plutôt que d'être hard-codé en dur comme je l'avais fait à l'époque, cf a1099d8
L'inscription "ne pas modifier", c'était pour éviter qu'un proprio de station ne s'amuse pas à le bouger sans savoir que ça peut avoir des conséquences : je pense d'ailleurs que côté IC ça servait lors du décodage des fichiers txt et ça doit même se retrouver dans un champ source quelque part en BDD :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oui, j'ai hésité aussi, d'ailleurs l'IA n'a pas voulu le modifier. vu le commentaire 🤣
C'était une décision de ma part et je me suis dit que vous me taperiez dessus au besoin. Mais le changement de code était trop conséquent pour ne pas itérer une version, de mon point de vue.
Sauf si c'est une version du "format de fichier généré" et non pas du script lui-même.
Dans ce cas en effet, vu que mon code génère le même format, il faut le laisser en 2.5 et versionner le script différemment.

@jlecordier

Copy link
Copy Markdown
Contributor

Bonjour @mhoareau, merci pour la PR

Quelques questions au préalable :

  • Quel est le réel besoin de cette PR ?
  • Quel est le changement minimal requis pour répondre à ce besoin ?
  • Qu'est-ce qui a été fait avec de l'IA, qu'est-ce qui a été fait sans ?
  • Quelle IA / modèle / effort ?
  • Pourrais-tu me partager l'historique des prompts et des réponses hors code ?
  • D'où viennent les snapshots en .txt ?

Le config.php a été renommé, donc si les gens mettent à jour le repo, ils auront un problème car leur config.php local est modifié

@mhoareau

Copy link
Copy Markdown
Author

Bonjour,

  • Quel est le réel besoin de cette PR ?
    Le besoin d'était d'assurer une compatibilité maximale avec les dernières versions de PHP (pas mal d'écritures/méthode dépréciés) pour l'installation sur des systèmes Raspberry OS récent (Trixie).
    J'allais juste faire des modifications "à la marge", mais je n'aime pas modifier du code non testé.
    Donc je me suis dis "OK, on commence par mettre l'existant sous test".

Puis une réorganisation de code (en fonction testables) était nécessaire pour couvrir 100% du code avec les tests (avec le premier filet de sécurité des premiers tests écrits).

Au début, je voulais juste viser la dernière version de PHP, en me disant que ça serait de toute façon pour l'embarquer sur les stations de notre réseau (MétéoR OI) qui auront toutes des versions de PHP récentes.
Mais je me suis dit que c'était dommage de faire ce travail sans le reverser.

Mais... après réflexion, je me suis dit que si je le reverse, il faut que ça reste compatible avec votre réseau existant par sécurité, donc finalement PHP 5.6 à 8.5.

Voilà un peu le cheminement qui a amené à cela.

  • Quel est le changement minimal requis pour répondre à ce besoin ?

Donc, pour le changement minimal, ça aurait été quelques lignes modifiées pour rendre le code compatible avec les nouvelles versions de PHP, sans warning, mais en rendant le code non compatible avec d'anciennes versions. Donc sans vous le reverser.

C'est toujours mieux d'avoir du code testé (à 100%) pour s'assurer que lors de futurs évolutions (qui seront de toute façon obligatoires avec les prochaines version de PHP), on ne casse rien.

  • Qu'est-ce qui a été fait avec de l'IA, qu'est-ce qui a été fait sans ?

Concernant l'IA, les tests sont développés à 100% avec l'IA (extrêmement efficace pour cela), la réécriture à 90% avec l'IA. En grande majorité Claude Code pour le développement des tests et du code, et le duo Claude Code/Codex pour les code reviews.
Toute la partie en amont (cadrage, planification, objectif, méthodologie) et la validation des reviews ont été faites par moi.

  • Pourrais-tu me partager l'historique des prompts et des réponses hors code ?

Pour les prompts, je verrais si j'ai la possibilité/le temps de vous les ressortir, mais le plan étant rédigé en fichier temporaire et faisant le ménage très régulièrement dans les fichiers mémoires/historiques pour forcer l'IA à ne pas avoir de biais, pas sûr de pouvoir vous reconstituer cela, mais je vais voir ce que je peux retrouver.

  • D'où viennent les snapshots en .txt ?

Les snapshots on été reconstitués (simulés) à partir de ce qui était généré par le script original, ils permettent de s'assurer qu'au fil des modifications/réorganisations de code, le script sort toujours le fichier attendu avec les valeurs dans les formats attendus.

Voilà pour les réponses
Aucun soucis si vous n'acceptez pas la PR, je l'ai surtout fait pour nous (MétéoR-OI), et je me suis dit que ça serait dommage de ne pas vous en faire profiter (code plus robuste et plus compatible).
Bonne soirée à vous.

@jlecordier

jlecordier commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Merci pour ces réponses détaillées

Concernant :

  • Quel est le réel besoin de cette PR ?
    Le besoin d'était d'assurer une compatibilité maximale avec les dernières versions de PHP (pas mal d'écritures/méthode dépréciés) pour l'installation sur des systèmes Raspberry OS récent (Trixie).

Le code actuel ne fonctionnait pas du tout ? Ou c’était juste des warnings ?

Concernant :

  • Quel est le changement minimal requis pour répondre à ce besoin ?

Donc, pour le changement minimal, ça aurait été quelques lignes modifiées pour rendre le code compatible avec les nouvelles versions de PHP, sans warning, mais en rendant le code non compatible avec d'anciennes versions. Donc sans vous le reverser.

Pourrais-tu me refaire une PR avec ces corrections minimales ? Elle ne sera pas forcément merged, mais c'est pour que je puisse y jeter un œil. Pourquoi est-ce que le nouveau code serait incompatible avec les anciennes versions ? Si ça rend incompatible, comment fait cette PR pour que ce soit compatible avec toutes les versions du coup ?

@jlecordier

Copy link
Copy Markdown
Contributor

Une autre solution serait d'avoir deux repos ou deux dossiers, un pour la version legacy, un avec ton nouveau code, comme ça on prend zéro risque, les déploiements actuels ne sont pas impactés, et les plus courageux peuvent utiliser la nouvelle version. Je suis vraiment une brêle en déploiement, donc j'essaie de limiter les impacts 😛

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants