[ADD] commentaires : un relevé non bloquant, sa règle, le code nettoyé
Rien ne relevait les commentaires hors convention. L'outil lit commentaires et docstrings, jamais le code autour, et rend deux familles inégales : l'identifiant — adresse, courriel, chemin de compte, nom de la liste privée — est une trouvaille ; le récit — témoignage, date, personne — un signal à relire. Le hook pre-commit le lance sur l'index, sort toujours en 0 — un contrôle bloquant à cette échelle se fait désinstaller — et parle quand l'outil échoue. La règle du générateur et sa doc portent le nettoyage au fur et à mesure, et l'exemple d'un interdit s'invente : base, adresse, compte et hôte en prennent un. L'écran du contexte est posé, sans entrée de menu. Vérifié : 252 tests des six fichiers d'essai touchés passent. --- EN --- Nothing reported the comments that break the convention. The tool reads comments and docstrings, never the code around them, and returns two unequal families: identifying data — address, e-mail, account path, private-list name — is a finding; narrative — witness marker, date, person — a signal to re-read. The pre-commit hook runs it on the index, always exits 0 — a blocking check at that scale gets uninstalled — and speaks when the tool fails. The generator rule and its doc carry the clean-up as you go, and a forbidden thing's example is invented: a real database, address, account and host each take one. The context screen is in place, with no menu entry. Checked: 252 tests of the six touched fixture files pass. Assisted-by: Claude Opus 5
This commit is contained in:
parent
7844387fa1
commit
0e5441ac39
20 changed files with 1341 additions and 53 deletions
|
|
@ -36,6 +36,13 @@ propriétaire, pas ses clients. Généraliser plutôt que censurer — « sur un
|
||||||
base de production », « sur un hôte qui exige une authentification sudo
|
base de production », « sur un hôte qui exige une authentification sudo
|
||||||
interactive » — dit la CLASSE de situation, qui est ce qui sert au lecteur.
|
interactive » — dit la CLASSE de situation, qui est ce qui sert au lecteur.
|
||||||
|
|
||||||
|
**L'exemple qui illustre un interdit s'invente.** La règle a d'abord été
|
||||||
|
violée par ses propres tests : pour démontrer qu'une adresse et un chemin de
|
||||||
|
compte sont refusés, ils en portaient de vrais, pris dans le parc. Choisir un
|
||||||
|
cas réel « parce qu'il est parlant » est exactement le réflexe que la règle
|
||||||
|
combat, et un test le fige pour toujours. Une valeur inventée démontre aussi
|
||||||
|
bien ; vérifier qu'elle n'existe nulle part ailleurs dans le dépôt.
|
||||||
|
|
||||||
Le récit n'est pas perdu, il change de place : l'enquête, les mesures datées
|
Le récit n'est pas perdu, il change de place : l'enquête, les mesures datées
|
||||||
et les impasses vivent dans `tasks/`, qui n'est pas versionné. Ni le fichier
|
et les impasses vivent dans `tasks/`, qui n'est pas versionné. Ni le fichier
|
||||||
ni le corps du commit ne les portent.
|
ni le corps du commit ne les portent.
|
||||||
|
|
@ -107,11 +114,8 @@ reste préférable quand elle tient.
|
||||||
|
|
||||||
Un garde-fou refuse le mécanique. Sur le sujet : tag absent, plus de 72
|
Un garde-fou refuse le mécanique. Sur le sujet : tag absent, plus de 72
|
||||||
caractères, ouverture sur une citation. Sur le corps : plus de 10 lignes pour
|
caractères, ouverture sur une citation. Sur le corps : plus de 10 lignes pour
|
||||||
une langue, une adresse IP, un courriel, un chemin de compte, et tout terme de
|
une langue, une adresse IP, un courriel, un chemin de compte. Ce qui reste un
|
||||||
`private/noms_interdits.txt` — la liste des clients et des machines, qui ne
|
jugement — « ce corps raconte-t-il l'enquête » — n'est vérifié par personne.
|
||||||
peut pas vivre dans git puisque c'est ce qu'elle protège. Absente, ce dernier
|
|
||||||
contrôle est muet. Ce qui reste un jugement — « ce corps raconte-t-il
|
|
||||||
l'enquête » — n'est vérifié par personne.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git config core.hooksPath script/git/hooks # une fois par clone
|
git config core.hooksPath script/git/hooks # une fois par clone
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,39 @@ ERPLibre inclut un système de génération de modules Odoo :
|
||||||
- `addons/TechnoLibre_odoo-code-generator-template/` — Templates
|
- `addons/TechnoLibre_odoo-code-generator-template/` — Templates
|
||||||
|
|
||||||
Documentation : `doc/CODE_GENERATOR.md`
|
Documentation : `doc/CODE_GENERATOR.md`
|
||||||
|
|
||||||
|
## Les commentaires du code produit
|
||||||
|
|
||||||
|
Le code généré porte des commentaires comme le reste, et la même règle : ils
|
||||||
|
disent COMMENT ça marche, ils ne portent rien d'identifiant et ils ne
|
||||||
|
racontent pas l'enquête. Voir `.claude/rules/04-code-conventions.md`.
|
||||||
|
|
||||||
|
Un module généré à partir d'une base existante hérite de ce qu'elle contient :
|
||||||
|
relire ses commentaires et ses docstrings avant de committer, un nom de client
|
||||||
|
ou de base y arrive tout seul.
|
||||||
|
|
||||||
|
## Le nettoyage au fur et à mesure
|
||||||
|
|
||||||
|
Rien ne se nettoie en une passe : on corrige les commentaires du fichier qu'on
|
||||||
|
touche, au moment où on le touche. Deux outils le rappellent.
|
||||||
|
|
||||||
|
Le hook `pre-commit` liste ce qui est à relire dans les fichiers indexés,
|
||||||
|
SANS bloquer le commit :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath script/git/hooks # une fois par clone
|
||||||
|
```
|
||||||
|
|
||||||
|
L'outil se lance aussi à la main, sur un fichier, un répertoire ou l'index :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
||||||
|
python3 script/analyse/check_comment_hygiene.py --staged
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
||||||
|
```
|
||||||
|
|
||||||
|
🔴 `identifiant` — adresse, courriel, chemin de compte : à retirer.
|
||||||
|
🟡 `récit` — témoignage, date, première personne : à RELIRE, l'outil ne
|
||||||
|
tranche pas. Un fait durable reste ; l'incident
|
||||||
|
où on l'a observé part. Codes de sortie : 0 rien, 1 des trouvailles, 2 l'outil
|
||||||
|
a échoué.
|
||||||
|
|
|
||||||
|
|
@ -138,11 +138,8 @@ That form is a fallback, not a default. Prefer the sentence when it fits.
|
||||||
|
|
||||||
**The guard rail.** `script/git/hooks/commit-msg` refuses a subject with no
|
**The guard rail.** `script/git/hooks/commit-msg` refuses a subject with no
|
||||||
tag, one over 72 characters, and one opening on a quotation. It reads the body
|
tag, one over 72 characters, and one opening on a quotation. It reads the body
|
||||||
too: over ten lines for one language, an IP address, an e-mail, a
|
too: over ten lines for one language, an IP address, an e-mail, and a
|
||||||
`/home/<account>/` path, or any term listed in `private/noms_interdits.txt` —
|
`/home/<account>/` path. Install the hook with `git config core.hooksPath script/git/hooks`; `git commit
|
||||||
the customer and machine names, which cannot live in git because they are what
|
|
||||||
the list protects. With no such file, that last check stays silent. Install
|
|
||||||
the hook with `git config core.hooksPath script/git/hooks`; `git commit
|
|
||||||
--no-verify` passes a legitimate exception. It checks only what is mechanical
|
--no-verify` passes a legitimate exception. It checks only what is mechanical
|
||||||
— whether the subject says what the code is about, and whether the body tells
|
— whether the subject says what the code is about, and whether the body tells
|
||||||
the story instead of the mechanism, stay judgements, and the tests above are
|
the story instead of the mechanism, stay judgements, and the tests above are
|
||||||
|
|
|
||||||
|
|
@ -1042,3 +1042,70 @@ Tester les générations des templates :
|
||||||
```bash
|
```bash
|
||||||
make test_code_generator_template
|
make test_code_generator_template
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- [en] -->
|
||||||
|
## Comments in generated code
|
||||||
|
|
||||||
|
Generated code carries comments like any other, and the same rule applies: a
|
||||||
|
comment says HOW the code works. It never carries identifying data — no
|
||||||
|
customer or third-party organisation, no real database name, no machine, no
|
||||||
|
address, no label or figure taken from a customer's data — and it does not
|
||||||
|
tell the story of the investigation that produced it.
|
||||||
|
|
||||||
|
A module generated from an existing database inherits what that database
|
||||||
|
holds: re-read its comments and docstrings before committing. A customer or
|
||||||
|
database name gets in on its own.
|
||||||
|
|
||||||
|
## Cleaning up as you go
|
||||||
|
|
||||||
|
Nothing gets cleaned in one sweep. You fix the comments of the file you are
|
||||||
|
touching, at the moment you touch it. A `pre-commit` hook lists what is worth
|
||||||
|
re-reading in the files you staged, and never blocks the commit:
|
||||||
|
|
||||||
|
<!-- [fr] -->
|
||||||
|
## Les commentaires du code produit
|
||||||
|
|
||||||
|
Le code généré porte des commentaires comme le reste, et la même règle
|
||||||
|
s'applique : un commentaire dit COMMENT le code marche. Il ne porte jamais de
|
||||||
|
donnée identifiante — ni client ni organisation tierce, ni nom de base réelle,
|
||||||
|
ni machine, ni adresse, ni libellé ou chiffre tiré des données d'un client — et
|
||||||
|
il ne raconte pas l'enquête qui l'a produit.
|
||||||
|
|
||||||
|
Un module généré à partir d'une base existante hérite de ce qu'elle contient :
|
||||||
|
relire ses commentaires et ses docstrings avant de committer. Un nom de client
|
||||||
|
ou de base y arrive tout seul.
|
||||||
|
|
||||||
|
## Le nettoyage au fur et à mesure
|
||||||
|
|
||||||
|
Rien ne se nettoie en une passe. On corrige les commentaires du fichier qu'on
|
||||||
|
touche, au moment où on le touche. Un hook `pre-commit` liste ce qui est à
|
||||||
|
relire dans les fichiers indexés, et ne bloque jamais le commit :
|
||||||
|
|
||||||
|
<!-- [common] -->
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath script/git/hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- [en] -->
|
||||||
|
The same tool runs by hand, on a file, a directory or the index. It reports
|
||||||
|
`identifiant` findings — an address, an e-mail, an account path — which are to
|
||||||
|
be removed, and `récit` signals —
|
||||||
|
a witness marker, a date, the first person — which are to be RE-READ: a
|
||||||
|
durable fact stays, the incident where it was observed goes. Exit codes follow
|
||||||
|
the repository convention: 0 nothing to report, 1 findings, 2 the tool failed.
|
||||||
|
|
||||||
|
<!-- [fr] -->
|
||||||
|
Le même outil se lance à la main, sur un fichier, un répertoire ou l'index. Il
|
||||||
|
signale les trouvailles `identifiant` — adresse, courriel, chemin de compte —
|
||||||
|
qui sont à retirer, et les signaux
|
||||||
|
`récit` — marqueur de témoignage, date, première personne — qui sont à RELIRE :
|
||||||
|
un fait durable reste, l'incident où on l'a observé part. Les codes de sortie
|
||||||
|
suivent la convention du dépôt : 0 rien à signaler, 1 des trouvailles, 2
|
||||||
|
l'outil a échoué.
|
||||||
|
|
||||||
|
<!-- [common] -->
|
||||||
|
```bash
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
||||||
|
python3 script/analyse/check_comment_hygiene.py --staged
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -649,3 +649,39 @@ Tester les générations des templates :
|
||||||
```bash
|
```bash
|
||||||
make test_code_generator_template
|
make test_code_generator_template
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Les commentaires du code produit
|
||||||
|
|
||||||
|
Le code généré porte des commentaires comme le reste, et la même règle
|
||||||
|
s'applique : un commentaire dit COMMENT le code marche. Il ne porte jamais de
|
||||||
|
donnée identifiante — ni client ni organisation tierce, ni nom de base réelle,
|
||||||
|
ni machine, ni adresse, ni libellé ou chiffre tiré des données d'un client — et
|
||||||
|
il ne raconte pas l'enquête qui l'a produit.
|
||||||
|
|
||||||
|
Un module généré à partir d'une base existante hérite de ce qu'elle contient :
|
||||||
|
relire ses commentaires et ses docstrings avant de committer. Un nom de client
|
||||||
|
ou de base y arrive tout seul.
|
||||||
|
|
||||||
|
## Le nettoyage au fur et à mesure
|
||||||
|
|
||||||
|
Rien ne se nettoie en une passe. On corrige les commentaires du fichier qu'on
|
||||||
|
touche, au moment où on le touche. Un hook `pre-commit` liste ce qui est à
|
||||||
|
relire dans les fichiers indexés, et ne bloque jamais le commit :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath script/git/hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
Le même outil se lance à la main, sur un fichier, un répertoire ou l'index. Il
|
||||||
|
signale les trouvailles `identifiant` — adresse, courriel, chemin de compte —
|
||||||
|
qui sont à retirer, et les signaux
|
||||||
|
`récit` — marqueur de témoignage, date, première personne — qui sont à RELIRE :
|
||||||
|
un fait durable reste, l'incident où on l'a observé part. Les codes de sortie
|
||||||
|
suivent la convention du dépôt : 0 rien à signaler, 1 des trouvailles, 2
|
||||||
|
l'outil a échoué.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
||||||
|
python3 script/analyse/check_comment_hygiene.py --staged
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
||||||
|
```
|
||||||
|
|
@ -414,3 +414,38 @@ TODO
|
||||||
```bash
|
```bash
|
||||||
make test_code_generator_template
|
make test_code_generator_template
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Comments in generated code
|
||||||
|
|
||||||
|
Generated code carries comments like any other, and the same rule applies: a
|
||||||
|
comment says HOW the code works. It never carries identifying data — no
|
||||||
|
customer or third-party organisation, no real database name, no machine, no
|
||||||
|
address, no label or figure taken from a customer's data — and it does not
|
||||||
|
tell the story of the investigation that produced it.
|
||||||
|
|
||||||
|
A module generated from an existing database inherits what that database
|
||||||
|
holds: re-read its comments and docstrings before committing. A customer or
|
||||||
|
database name gets in on its own.
|
||||||
|
|
||||||
|
## Cleaning up as you go
|
||||||
|
|
||||||
|
Nothing gets cleaned in one sweep. You fix the comments of the file you are
|
||||||
|
touching, at the moment you touch it. A `pre-commit` hook lists what is worth
|
||||||
|
re-reading in the files you staged, and never blocks the commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath script/git/hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
The same tool runs by hand, on a file, a directory or the index. It reports
|
||||||
|
`identifiant` findings — an address, an e-mail, an account path — which are to
|
||||||
|
be removed, and `récit` signals —
|
||||||
|
a witness marker, a date, the first person — which are to be RE-READ: a
|
||||||
|
durable fact stays, the incident where it was observed goes. Exit codes follow
|
||||||
|
the repository convention: 0 nothing to report, 1 findings, 2 the tool failed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
||||||
|
python3 script/analyse/check_comment_hygiene.py --staged
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script --identifying-only
|
||||||
|
```
|
||||||
432
script/analyse/check_comment_hygiene.py
Normal file
432
script/analyse/check_comment_hygiene.py
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
|
||||||
|
"""Les commentaires d'un fichier disent-ils le fonctionnement, ou son contexte ?
|
||||||
|
|
||||||
|
La convention est dans `.claude/rules/04-code-conventions.md` : un commentaire
|
||||||
|
dit COMMENT le code marche, il ne porte aucune donnée identifiante et il ne
|
||||||
|
raconte pas l'enquête. Cet outil en vérifie la part mécanique.
|
||||||
|
|
||||||
|
Deux familles, de sûreté très différente :
|
||||||
|
|
||||||
|
- `identifiant` — adresse IP, courriel, chemin de compte. Une
|
||||||
|
correspondance est une trouvaille : ces formes n'ont aucune raison d'être
|
||||||
|
dans un commentaire.
|
||||||
|
- `récit` — marqueur de témoignage (« vécu sur », « mesuré le »), date
|
||||||
|
absolue, première personne. Une correspondance est un SIGNAL À RELIRE : la
|
||||||
|
même phrase peut énoncer un fait durable. L'outil ne trie pas à la place
|
||||||
|
du lecteur.
|
||||||
|
|
||||||
|
Il lit les commentaires `#` et, en Python, les docstrings de module, de classe
|
||||||
|
et de fonction. Le reste du code ne l'intéresse pas.
|
||||||
|
|
||||||
|
Il se signale lui-même : ce fichier CITE les marqueurs qu'il cherche, et ses
|
||||||
|
citations sont des correspondances comme les autres. Ces trouvailles-là sont
|
||||||
|
la définition de l'outil, pas un défaut à corriger.
|
||||||
|
|
||||||
|
Codes de sortie, convention partagée des outils du dépôt : 0 rien à signaler,
|
||||||
|
1 des trouvailles, 2 l'outil a échoué.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ast
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tokenize
|
||||||
|
|
||||||
|
RACINE = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
|
||||||
|
sys.path.append(RACINE)
|
||||||
|
sys.path.append(os.path.join(RACINE, "script"))
|
||||||
|
|
||||||
|
# Réexportés pour que ce module reste le seul point d'entrée de l'outil.
|
||||||
|
from lib_identifiant import ( # noqa: E402,F401
|
||||||
|
NOMS_INTERDITS,
|
||||||
|
adresse_de_machine,
|
||||||
|
identifiants,
|
||||||
|
termes_interdits,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from script.todo.todo_i18n import t
|
||||||
|
except Exception: # pragma: no cover - repli si i18n indisponible
|
||||||
|
|
||||||
|
def t(key: str) -> str:
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
SUFFIXES = (".py", ".sh", ".bash")
|
||||||
|
|
||||||
|
# Ce qui vient d'ailleurs ou n'est pas du source : le dépôt ne le réécrit pas.
|
||||||
|
EXCLUS = (
|
||||||
|
"/OCA_",
|
||||||
|
"/addons/",
|
||||||
|
"/.venv",
|
||||||
|
"/node_modules/",
|
||||||
|
"/.git/",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def a_balayer(chemin):
|
||||||
|
"""Un chemin du dépôt, et non du code tiers ou un environnement."""
|
||||||
|
normalise = chemin.replace(os.sep, "/")
|
||||||
|
while normalise.startswith("./"):
|
||||||
|
normalise = normalise[2:]
|
||||||
|
return not any(exclu in "/" + normalise for exclu in EXCLUS)
|
||||||
|
|
||||||
|
|
||||||
|
# Le témoignage : la phrase prend un événement pour sujet au lieu du code.
|
||||||
|
# L'accent porte la distinction : « mesuré sur » témoigne, « mesure le »
|
||||||
|
# décrit ce que le code fait. Sans lui, le motif prend le présent pour du passé.
|
||||||
|
# « le relevé » est un NOM et prend les mêmes prépositions : un déterminant
|
||||||
|
# devant le participe désigne la chose relevée, pas l'acte de relever.
|
||||||
|
RECIT = re.compile(
|
||||||
|
r"(?<!\w)(?<!(?:le|du|au|un|ce)\s)(?<!(?:son|des|les|aux|mon|ton)\s)"
|
||||||
|
r"(?:vécu|mesuré|relevé|rapporté|constaté|observé|signalé)e?s?"
|
||||||
|
r"(?:[,]?\s+(?:sur|le|la|les|dans|chez|par|au|aux|en|à|ce|deux|trois)\b"
|
||||||
|
r"|\s*[:—])",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Une date absolue date le commentaire : le fonctionnement, lui, n'a pas de date.
|
||||||
|
DATE = re.compile(
|
||||||
|
r"\b\d{4}-\d{2}-\d{2}\b"
|
||||||
|
r"|\b\d{1,2}\s+(?:janvier|février|mars|avril|mai|juin|juillet|août"
|
||||||
|
r"|septembre|octobre|novembre|décembre)\s+\d{4}\b",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Le rédacteur pris pour sujet : « ma conclusion était fausse », « mes essais ».
|
||||||
|
PERSONNE = re.compile(
|
||||||
|
r"(?<![\w'])(?:j'|je|ma|mon|mes|nous\s+avons"
|
||||||
|
r"|ce\s+matin|la\s+veille|ce\s+jour-là|hier)\b",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
MOTIFS_RECIT = (
|
||||||
|
("témoignage", RECIT),
|
||||||
|
("date", DATE),
|
||||||
|
("personne", PERSONNE),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _bloc(sous_lignes):
|
||||||
|
"""Un bloc : son texte recollé, et les lignes physiques qui le portent.
|
||||||
|
|
||||||
|
Le texte recollé sert à CHERCHER — une phrase coupée en trois lignes reste
|
||||||
|
une phrase. Chaque sous-ligne garde en plus son OFFSET dans ce texte : une
|
||||||
|
occurrence trouvée à la position n se convertit alors en numéro de ligne
|
||||||
|
sans jamais rechercher l'extrait, qui se retrouverait dans un autre mot.
|
||||||
|
"""
|
||||||
|
debuts, position = [], 0
|
||||||
|
for numero, texte in sous_lignes:
|
||||||
|
debuts.append((position, numero))
|
||||||
|
position += len(texte) + 1
|
||||||
|
return {
|
||||||
|
"line": sous_lignes[0][0],
|
||||||
|
"text": " ".join(texte for _, texte in sous_lignes),
|
||||||
|
"lines": sous_lignes,
|
||||||
|
"offsets": debuts,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _regroupe(lignes_commentees):
|
||||||
|
"""Les lignes consécutives forment un bloc ; un trou en ouvre un autre."""
|
||||||
|
blocs, courant = [], []
|
||||||
|
for numero, texte in lignes_commentees:
|
||||||
|
if courant and numero == courant[-1][0] + 1:
|
||||||
|
courant.append((numero, texte))
|
||||||
|
else:
|
||||||
|
if courant:
|
||||||
|
blocs.append(_bloc(courant))
|
||||||
|
courant = [(numero, texte)]
|
||||||
|
if courant:
|
||||||
|
blocs.append(_bloc(courant))
|
||||||
|
return blocs
|
||||||
|
|
||||||
|
|
||||||
|
def blocs_python(source):
|
||||||
|
"""Les commentaires et docstrings d'un source Python.
|
||||||
|
|
||||||
|
Une docstring garde ses lignes PHYSIQUES, relues dans le source : le
|
||||||
|
littéral de l'AST a perdu son indentation et ses numéros.
|
||||||
|
"""
|
||||||
|
lignes_source = source.split("\n")
|
||||||
|
|
||||||
|
commentaires = []
|
||||||
|
lisible = True
|
||||||
|
try:
|
||||||
|
for jeton in tokenize.generate_tokens(io.StringIO(source).readline):
|
||||||
|
if jeton.type == tokenize.COMMENT:
|
||||||
|
commentaires.append(
|
||||||
|
(jeton.start[0], jeton.string.lstrip("#").strip())
|
||||||
|
)
|
||||||
|
except (tokenize.TokenError, IndentationError, SyntaxError):
|
||||||
|
# Un source que Python refuse reste du texte : le balayage ligne à
|
||||||
|
# ligne voit encore les « # », là où rendre un rapport vide dirait
|
||||||
|
# « propre » d'un fichier qu'on n'a pas lu.
|
||||||
|
lisible = False
|
||||||
|
trouves = _regroupe(commentaires) if lisible else blocs_shell(source)
|
||||||
|
|
||||||
|
try:
|
||||||
|
arbre = ast.parse(source)
|
||||||
|
except SyntaxError:
|
||||||
|
return sorted(trouves, key=lambda b: b["line"])
|
||||||
|
|
||||||
|
portees = [arbre] + [
|
||||||
|
noeud
|
||||||
|
for noeud in ast.walk(arbre)
|
||||||
|
if isinstance(
|
||||||
|
noeud, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for noeud in portees:
|
||||||
|
corps = getattr(noeud, "body", None)
|
||||||
|
if not corps or not isinstance(corps[0], ast.Expr):
|
||||||
|
continue
|
||||||
|
valeur = corps[0].value
|
||||||
|
if not (
|
||||||
|
isinstance(valeur, ast.Constant) and isinstance(valeur.value, str)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
debut = corps[0].lineno
|
||||||
|
fin = getattr(corps[0], "end_lineno", debut) or debut
|
||||||
|
sous_lignes = [
|
||||||
|
(numero, lignes_source[numero - 1].strip())
|
||||||
|
for numero in range(debut, min(fin, len(lignes_source)) + 1)
|
||||||
|
]
|
||||||
|
if sous_lignes:
|
||||||
|
trouves.append(_bloc(sous_lignes))
|
||||||
|
|
||||||
|
return sorted(trouves, key=lambda b: b["line"])
|
||||||
|
|
||||||
|
|
||||||
|
def commentaire_shell(ligne):
|
||||||
|
"""Ce que dit le commentaire de cette ligne shell, ou None.
|
||||||
|
|
||||||
|
Le « # » qui ouvre un commentaire est celui qui n'est ni protégé par des
|
||||||
|
guillemets, ni échappé, ni collé à un mot — `${VAR#préfixe}` et une URL
|
||||||
|
`…/#ancre` n'ouvrent rien. Le shebang n'est pas un commentaire.
|
||||||
|
"""
|
||||||
|
if ligne.lstrip().startswith("#!"):
|
||||||
|
return None
|
||||||
|
quote = None
|
||||||
|
precedent = ""
|
||||||
|
for index, caractere in enumerate(ligne):
|
||||||
|
if precedent == "\\":
|
||||||
|
precedent = ""
|
||||||
|
continue
|
||||||
|
if quote:
|
||||||
|
if caractere == quote:
|
||||||
|
quote = None
|
||||||
|
elif caractere in "\"'":
|
||||||
|
quote = caractere
|
||||||
|
elif caractere == "#" and (index == 0 or ligne[index - 1].isspace()):
|
||||||
|
return ligne[index:].lstrip("#").strip()
|
||||||
|
precedent = caractere
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def blocs_shell(source):
|
||||||
|
"""Les commentaires d'un script shell, les consécutifs regroupés."""
|
||||||
|
commentaires = []
|
||||||
|
for numero, ligne in enumerate(source.split("\n"), start=1):
|
||||||
|
texte = commentaire_shell(ligne)
|
||||||
|
if texte is not None:
|
||||||
|
commentaires.append((numero, texte))
|
||||||
|
return _regroupe(commentaires)
|
||||||
|
|
||||||
|
|
||||||
|
def blocs(chemin, source):
|
||||||
|
"""Les commentaires d'un fichier, selon son suffixe."""
|
||||||
|
if chemin.endswith(".py"):
|
||||||
|
return blocs_python(source)
|
||||||
|
return blocs_shell(source)
|
||||||
|
|
||||||
|
|
||||||
|
def recits(texte):
|
||||||
|
"""Les marqueurs de récit d'un texte : (motif, extrait, position).
|
||||||
|
|
||||||
|
Toutes les occurrences, et non la première : un bloc de vingt lignes en
|
||||||
|
porte souvent plusieurs, et n'en montrer qu'une cache le reste du travail.
|
||||||
|
"""
|
||||||
|
trouves = []
|
||||||
|
for nom, motif in MOTIFS_RECIT:
|
||||||
|
for trouve in motif.finditer(texte):
|
||||||
|
trouves.append((nom, trouve.group(0).strip(), trouve.start()))
|
||||||
|
return sorted(trouves, key=lambda t: t[2])
|
||||||
|
|
||||||
|
|
||||||
|
def ligne_a(bloc, position):
|
||||||
|
"""La ligne physique qui porte cette position du texte recollé."""
|
||||||
|
numero = bloc["line"]
|
||||||
|
for debut, ligne in bloc["offsets"]:
|
||||||
|
if debut > position:
|
||||||
|
break
|
||||||
|
numero = ligne
|
||||||
|
return numero
|
||||||
|
|
||||||
|
|
||||||
|
def inspect(chemin, source=None, termes=None):
|
||||||
|
"""Les trouvailles d'un fichier, dans l'ordre des lignes."""
|
||||||
|
if termes is None:
|
||||||
|
termes = termes_interdits()
|
||||||
|
if source is None:
|
||||||
|
with io.open(chemin, encoding="utf-8", errors="replace") as fh:
|
||||||
|
source = fh.read()
|
||||||
|
|
||||||
|
trouvailles = []
|
||||||
|
for bloc in blocs(chemin, source):
|
||||||
|
familles = (
|
||||||
|
("identifiant", identifiants(bloc["text"], termes)),
|
||||||
|
("récit", recits(bloc["text"])),
|
||||||
|
)
|
||||||
|
for genre, trouves in familles:
|
||||||
|
for motif, extrait, position in trouves:
|
||||||
|
trouvailles.append(
|
||||||
|
{
|
||||||
|
"file": chemin,
|
||||||
|
"line": ligne_a(bloc, position),
|
||||||
|
"kind": genre,
|
||||||
|
"pattern": motif,
|
||||||
|
"excerpt": extrait,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return sorted(trouvailles, key=lambda f: (f["line"], f["kind"]))
|
||||||
|
|
||||||
|
|
||||||
|
def fichiers_indexes():
|
||||||
|
"""Les fichiers ajoutés à l'index git, filtrés sur les suffixes lisibles."""
|
||||||
|
sortie = subprocess.run(
|
||||||
|
["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=RACINE,
|
||||||
|
)
|
||||||
|
chemins = []
|
||||||
|
for nom in sortie.stdout.split("\n"):
|
||||||
|
nom = nom.strip()
|
||||||
|
if (
|
||||||
|
nom.endswith(SUFFIXES)
|
||||||
|
and a_balayer(nom)
|
||||||
|
and os.path.isfile(os.path.join(RACINE, nom))
|
||||||
|
):
|
||||||
|
chemins.append(nom)
|
||||||
|
return chemins
|
||||||
|
|
||||||
|
|
||||||
|
def etend(chemins):
|
||||||
|
"""Les fichiers lisibles d'une liste de chemins, répertoires parcourus."""
|
||||||
|
trouves = []
|
||||||
|
for chemin in chemins:
|
||||||
|
if os.path.isdir(chemin):
|
||||||
|
for base, _, noms in os.walk(chemin):
|
||||||
|
if not a_balayer(base + "/"):
|
||||||
|
continue
|
||||||
|
for nom in sorted(noms):
|
||||||
|
complet = os.path.join(base, nom)
|
||||||
|
if nom.endswith(SUFFIXES) and a_balayer(complet):
|
||||||
|
trouves.append(complet)
|
||||||
|
elif chemin.endswith(SUFFIXES) and a_balayer(chemin):
|
||||||
|
trouves.append(chemin)
|
||||||
|
return trouves
|
||||||
|
|
||||||
|
|
||||||
|
def render(trouvailles, colour=True):
|
||||||
|
"""Le rapport, groupé par fichier."""
|
||||||
|
if not trouvailles:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def peindre(texte, code):
|
||||||
|
return f"\033[{code}m{texte}\033[0m" if colour else texte
|
||||||
|
|
||||||
|
lignes = []
|
||||||
|
fichier = None
|
||||||
|
for f in trouvailles:
|
||||||
|
if f["file"] != fichier:
|
||||||
|
fichier = f["file"]
|
||||||
|
lignes.append(peindre(fichier, "1"))
|
||||||
|
icone = "🔴" if f["kind"] == "identifiant" else "🟡"
|
||||||
|
lignes.append(
|
||||||
|
f" {icone} {f['line']:>5} {f['pattern']:<11} {f['excerpt']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
durs = sum(1 for f in trouvailles if f["kind"] == "identifiant")
|
||||||
|
mous = len(trouvailles) - durs
|
||||||
|
lignes.append("")
|
||||||
|
lignes.append(
|
||||||
|
t(
|
||||||
|
"%s identifying, %s to re-read — see .claude/rules/04-code-conventions.md"
|
||||||
|
)
|
||||||
|
% (peindre(durs, "31"), peindre(mous, "33"))
|
||||||
|
)
|
||||||
|
return "\n".join(lignes)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description=t(
|
||||||
|
"do the comments say how the code works, or where it came from"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser.add_argument("paths", nargs="*", default=[])
|
||||||
|
parser.add_argument(
|
||||||
|
"--staged",
|
||||||
|
action="store_true",
|
||||||
|
help=t("only the files added to the git index"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--identifying-only",
|
||||||
|
action="store_true",
|
||||||
|
help=t("drop the narrative signals, keep the certain findings"),
|
||||||
|
)
|
||||||
|
parser.add_argument("--json", action="store_true")
|
||||||
|
parser.add_argument("--no-color", action="store_true")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
|
if args.staged:
|
||||||
|
chemins = fichiers_indexes()
|
||||||
|
elif args.paths:
|
||||||
|
chemins = etend(args.paths)
|
||||||
|
else:
|
||||||
|
parser.error(t("give a path, or --staged"))
|
||||||
|
return 2
|
||||||
|
|
||||||
|
termes = termes_interdits()
|
||||||
|
trouvailles = []
|
||||||
|
for chemin in chemins:
|
||||||
|
try:
|
||||||
|
trouvailles.extend(inspect(chemin, termes=termes))
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"❌ {chemin} : {exc}", file=sys.stderr)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if args.identifying_only:
|
||||||
|
trouvailles = [f for f in trouvailles if f["kind"] == "identifiant"]
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(
|
||||||
|
json.dumps(
|
||||||
|
{"scanned": len(chemins), "findings": trouvailles},
|
||||||
|
indent=2,
|
||||||
|
ensure_ascii=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
colour = sys.stdout.isatty() and not args.no_color
|
||||||
|
rapport = render(trouvailles, colour)
|
||||||
|
if rapport:
|
||||||
|
print(rapport)
|
||||||
|
|
||||||
|
return 1 if trouvailles else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -14,8 +14,8 @@ Le reste — « ce sujet dit-il sur quoi porte le code », « ce corps raconte-t
|
||||||
l'enquête plutôt que le fonctionnement » — est un jugement, et aucun hook ne
|
l'enquête plutôt que le fonctionnement » — est un jugement, et aucun hook ne
|
||||||
le rendra.
|
le rendra.
|
||||||
|
|
||||||
Compté en CARACTÈRES et non en octets : « préchauffer » pèse 12 caractères et
|
Compté en CARACTÈRES et non en octets : « préchauffer » pèse 11 caractères et
|
||||||
14 octets, et une limite en octets refuserait des sujets français conformes.
|
12 octets, et une limite en octets refuserait des sujets français conformes.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -199,9 +199,9 @@ def _check_body(sans_trailers: str, avec_trailers: str) -> list:
|
||||||
noms = sorted(set(par_motif.get("nom privé", [])))
|
noms = sorted(set(par_motif.get("nom privé", [])))
|
||||||
if noms:
|
if noms:
|
||||||
problems.append(
|
problems.append(
|
||||||
f"le corps porte un nom de la liste privée : {', '.join(noms)}.\n"
|
f"le corps porte un nom refusé : {', '.join(noms)}.\n"
|
||||||
" Généralisez — « sur une base de production » — ou retirez la\n"
|
" Généralisez — « sur une base de production » — ou retirez la\n"
|
||||||
" phrase. La liste est dans private/noms_interdits.txt."
|
" phrase."
|
||||||
)
|
)
|
||||||
|
|
||||||
return problems
|
return problems
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ def main():
|
||||||
whitelist = []
|
whitelist = []
|
||||||
if config.from_backup_path or config.from_backup_name:
|
if config.from_backup_path or config.from_backup_name:
|
||||||
# script/database/get_repo_from_backup.py
|
# script/database/get_repo_from_backup.py
|
||||||
# --backup_name bpir_prod_5_dec_2025_2026-02-04_14h27m54s.zip
|
# --backup_name pir_prod_5_dec_2025_2026-02-04_14h27m54s.zip
|
||||||
if config.from_backup_path:
|
if config.from_backup_path:
|
||||||
cmd = f"./script/database/get_repo_from_backup.py --backup_path {config.from_backup_path}"
|
cmd = f"./script/database/get_repo_from_backup.py --backup_path {config.from_backup_path}"
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
80
script/git/hooks/pre-commit
Executable file
80
script/git/hooks/pre-commit
Executable file
|
|
@ -0,0 +1,80 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
"""Hook pre-commit : montre les commentaires hors convention des fichiers indexés.
|
||||||
|
|
||||||
|
Il INFORME et ne refuse jamais — le nettoyage se fait au fur et à mesure, sur
|
||||||
|
les fichiers qu'on touche déjà, et non en une passe qui réécrirait le dépôt.
|
||||||
|
Un hook qui bloquerait sur du style se ferait désinstaller la même semaine.
|
||||||
|
|
||||||
|
Installation :
|
||||||
|
|
||||||
|
git config core.hooksPath script/git/hooks
|
||||||
|
|
||||||
|
Il sort toujours en 0. Pour l'interroger à la main, sur un fichier ou un
|
||||||
|
répertoire :
|
||||||
|
|
||||||
|
python3 script/analyse/check_comment_hygiene.py script/todo/todo.py
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
RACINE = Path(__file__).resolve().parents[3]
|
||||||
|
OUTIL = RACINE / "script" / "analyse" / "check_comment_hygiene.py"
|
||||||
|
|
||||||
|
# Au-delà, le rapport cesse d'être une invitation et devient un mur.
|
||||||
|
PLAFOND = 12
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not OUTIL.is_file():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
sortie = subprocess.run(
|
||||||
|
[sys.executable, str(OUTIL), "--staged", "--no-color"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=str(RACINE),
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if sortie.returncode not in (0, 1):
|
||||||
|
# Jamais bloquant, mais jamais muet non plus : un outil cassé qui se
|
||||||
|
# tait se confond avec un dépôt propre.
|
||||||
|
print(
|
||||||
|
f"\n ⓘ check_comment_hygiene a échoué ({sortie.returncode}) ;"
|
||||||
|
" les commentaires n'ont pas été relus\n",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if sortie.returncode != 1 or not sortie.stdout.strip():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
lignes = sortie.stdout.rstrip("\n").split("\n")
|
||||||
|
trop = len(lignes) - PLAFOND
|
||||||
|
if trop > 0:
|
||||||
|
lignes = lignes[:PLAFOND] + [f" … et {trop} lignes de plus"]
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\n ⓘ commentaires à relire dans ce que vous committez\n",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
for ligne in lignes:
|
||||||
|
print(f" {ligne}", file=sys.stderr)
|
||||||
|
print(
|
||||||
|
"\n 🔴 identifiant : à retirer. 🟡 récit : à relire — un fait durable"
|
||||||
|
"\n reste, l'incident où on l'a vu part."
|
||||||
|
"\n La règle : .claude/rules/04-code-conventions.md"
|
||||||
|
"\n Le commit n'est PAS bloqué. Corrigez ce que vous touchez.\n",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
La règle est dans `.claude/rules/04-code-conventions.md` : ce qui est versionné
|
La règle est dans `.claude/rules/04-code-conventions.md` : ce qui est versionné
|
||||||
ne nomme ni client, ni base réelle, ni machine, ni adresse. Deux garde-fous s'en
|
ne nomme ni client, ni base réelle, ni machine, ni adresse. Deux garde-fous s'en
|
||||||
servent — `script/git/hooks/commit-msg` sur le message, et
|
servent — `script/git/commit_msg_lib.py` sur le message, et
|
||||||
`script/analyse/check_comment_hygiene.py` sur les commentaires — et ils
|
`script/analyse/check_comment_hygiene.py` sur les commentaires — et ils
|
||||||
partagent CE module. Deux prédicats séparés pour la même question dérivent.
|
partagent CE module. Deux prédicats séparés pour la même question dérivent.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2761,6 +2761,152 @@ class TODO(
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
print(f"{t('Total:')}" f" {len(files)}")
|
print(f"{t('Total:')}" f" {len(files)}")
|
||||||
|
|
||||||
|
def _claude_context_root(self):
|
||||||
|
"""La racine du dépôt, deux niveaux au-dessus de ce fichier."""
|
||||||
|
return os.path.normpath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compte_lignes(self, chemin):
|
||||||
|
"""Le nombre de lignes d'un fichier ; 0 s'il est illisible."""
|
||||||
|
try:
|
||||||
|
with open(chemin, encoding="utf-8", errors="replace") as fh:
|
||||||
|
return sum(1 for _ in fh)
|
||||||
|
except OSError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _claude_command_state(self, deployed, template):
|
||||||
|
"""La copie déployée d'une commande suit-elle encore le gabarit ?
|
||||||
|
|
||||||
|
La comparaison ignore les lignes qui portent une identité git : le
|
||||||
|
déploiement y substitue le nom et le courriel, et une égalité stricte
|
||||||
|
déclarerait périmée toute commande personnalisée.
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(template):
|
||||||
|
return t("not in the repository")
|
||||||
|
if not os.path.isfile(deployed):
|
||||||
|
return t("missing")
|
||||||
|
|
||||||
|
def stables(chemin):
|
||||||
|
with open(chemin, encoding="utf-8", errors="replace") as fh:
|
||||||
|
return [x for x in fh if "user.name=" not in x]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if stables(deployed) == stables(template):
|
||||||
|
return t("up to date")
|
||||||
|
except OSError:
|
||||||
|
return t("missing")
|
||||||
|
return t("redeploy needed")
|
||||||
|
|
||||||
|
def _claude_memory_dir(self):
|
||||||
|
"""Le répertoire de mémoire de Claude Code pour CE dépôt.
|
||||||
|
|
||||||
|
Le nom du projet est le chemin absolu dont chaque séparateur devient
|
||||||
|
un tiret : c'est la convention de Claude Code, pas la nôtre.
|
||||||
|
"""
|
||||||
|
racine = self._claude_context_root()
|
||||||
|
projet = racine.replace(os.sep, "-")
|
||||||
|
return os.path.expanduser(
|
||||||
|
os.path.join("~/.claude/projects", projet, "memory")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _show_claude_context(self):
|
||||||
|
"""Ce que Claude reçoit avant la première question : sources et état.
|
||||||
|
|
||||||
|
Rend None. Écrit un tableau et ne modifie rien. Ne relève que ce qui
|
||||||
|
est versionné ou déployé ; ce que `private/` contient n'y figure pas.
|
||||||
|
"""
|
||||||
|
racine = self._claude_context_root()
|
||||||
|
largeur = 62
|
||||||
|
print(f"🧠 {t('Context given to Claude')}")
|
||||||
|
print("-" * largeur)
|
||||||
|
|
||||||
|
instructions = os.path.join(racine, "CLAUDE.md")
|
||||||
|
if os.path.isfile(instructions):
|
||||||
|
n = self._compte_lignes(instructions)
|
||||||
|
print(f"{t('Instructions'):<22} CLAUDE.md {n} {t('lines')}")
|
||||||
|
else:
|
||||||
|
print(f"{t('Instructions'):<22} CLAUDE.md {t('missing')}")
|
||||||
|
|
||||||
|
regles = os.path.join(racine, ".claude", "rules")
|
||||||
|
if os.path.isdir(regles):
|
||||||
|
noms = sorted(f for f in os.listdir(regles) if f.endswith(".md"))
|
||||||
|
total = sum(
|
||||||
|
self._compte_lignes(os.path.join(regles, f)) for f in noms
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"{t('Rules'):<22} .claude/rules/ {len(noms)}"
|
||||||
|
f" {t('files')}, {total} {t('lines')}"
|
||||||
|
)
|
||||||
|
for nom in noms:
|
||||||
|
n = self._compte_lignes(os.path.join(regles, nom))
|
||||||
|
print(f"{'':<22} {nom:<28} {n} {t('lines')}")
|
||||||
|
else:
|
||||||
|
print(f"{t('Rules'):<22} .claude/rules/ {t('missing')}")
|
||||||
|
|
||||||
|
skills = os.path.join(racine, ".claude", "skills")
|
||||||
|
if os.path.isdir(skills):
|
||||||
|
noms = sorted(
|
||||||
|
d
|
||||||
|
for d in os.listdir(skills)
|
||||||
|
if os.path.isfile(os.path.join(skills, d, "SKILL.md"))
|
||||||
|
)
|
||||||
|
print(f"{t('Skills'):<22} .claude/skills/ {len(noms)}")
|
||||||
|
for nom in noms:
|
||||||
|
print(f"{'':<22} {nom}")
|
||||||
|
else:
|
||||||
|
print(f"{t('Skills'):<22} .claude/skills/ {t('missing')}")
|
||||||
|
|
||||||
|
print(f"{t('Deployed commands'):<22} ~/.claude/commands/")
|
||||||
|
gabarits = {
|
||||||
|
"commit": "template_claude_commands_commit.md",
|
||||||
|
"todo_add_command": "template_claude_commands_todo_add_command.md",
|
||||||
|
}
|
||||||
|
for nom, gabarit in sorted(gabarits.items()):
|
||||||
|
etat = self._claude_command_state(
|
||||||
|
os.path.expanduser(f"~/.claude/commands/{nom}.md"),
|
||||||
|
os.path.join(racine, "conf", gabarit),
|
||||||
|
)
|
||||||
|
print(f"{'':<22} /{nom:<26} {etat}")
|
||||||
|
|
||||||
|
chemin_hooks = self._git_hooks_path(racine)
|
||||||
|
print(
|
||||||
|
f"{t('Git hooks'):<22}"
|
||||||
|
f" {chemin_hooks or t('hook not installed')}"
|
||||||
|
)
|
||||||
|
if chemin_hooks:
|
||||||
|
absolu = os.path.join(racine, chemin_hooks)
|
||||||
|
for hook in ("commit-msg", "pre-commit"):
|
||||||
|
pose = os.access(os.path.join(absolu, hook), os.X_OK)
|
||||||
|
marque = (
|
||||||
|
t("hook installed") if pose else t("hook not installed")
|
||||||
|
)
|
||||||
|
print(f"{'':<22} {hook:<26} {marque}")
|
||||||
|
|
||||||
|
memoire = self._claude_memory_dir()
|
||||||
|
if os.path.isdir(memoire):
|
||||||
|
n = len([f for f in os.listdir(memoire) if f.endswith(".md")])
|
||||||
|
print(f"{t('Memory'):<22} ~/.claude/projects/…/memory/ {n}")
|
||||||
|
else:
|
||||||
|
print(f"{t('Memory'):<22} {t('missing')}")
|
||||||
|
|
||||||
|
print("-" * largeur)
|
||||||
|
|
||||||
|
def _git_hooks_path(self, racine):
|
||||||
|
"""La valeur de core.hooksPath, ou None si git n'en déclare aucune."""
|
||||||
|
try:
|
||||||
|
sortie = subprocess.run(
|
||||||
|
["git", "config", "--get", "core.hooksPath"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=racine,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return None
|
||||||
|
chemin = sortie.stdout.strip()
|
||||||
|
return chemin or None
|
||||||
|
|
||||||
def _setup_claude_command(
|
def _setup_claude_command(
|
||||||
self, command_name, template_filename, personalize=False
|
self, command_name, template_filename, personalize=False
|
||||||
):
|
):
|
||||||
|
|
@ -3694,7 +3840,7 @@ class TODO(
|
||||||
de modèles et de colonnes, et lesquels sont traduits ou uniques.
|
de modèles et de colonnes, et lesquels sont traduits ou uniques.
|
||||||
|
|
||||||
La confirmation redemande le NOM de la base. Une frappe sur « o »
|
La confirmation redemande le NOM de la base. Une frappe sur « o »
|
||||||
se donne par réflexe ; recopier « chezlepro_neutralize_upgrade_18 »
|
se donne par réflexe ; recopier « sireine_neutralize_upgrade_18 »
|
||||||
oblige à regarder ce qu'on détruit.
|
oblige à regarder ce qu'on détruit.
|
||||||
"""
|
"""
|
||||||
from script.analyse import monitoring
|
from script.analyse import monitoring
|
||||||
|
|
|
||||||
|
|
@ -6856,6 +6856,75 @@ TRANSLATIONS = {
|
||||||
"fr": "l'outil lui-même a échoué",
|
"fr": "l'outil lui-même a échoué",
|
||||||
"en": "the tool itself failed",
|
"en": "the tool itself failed",
|
||||||
},
|
},
|
||||||
|
# Contexte fourni à Claude
|
||||||
|
"Show the context given to Claude": {
|
||||||
|
"fr": "Afficher le contexte fourni à Claude",
|
||||||
|
"en": "Show the context given to Claude",
|
||||||
|
},
|
||||||
|
"Context given to Claude": {
|
||||||
|
"fr": "Contexte fourni à Claude",
|
||||||
|
"en": "Context given to Claude",
|
||||||
|
},
|
||||||
|
"Instructions": {
|
||||||
|
"fr": "Instructions",
|
||||||
|
"en": "Instructions",
|
||||||
|
},
|
||||||
|
"Rules": {
|
||||||
|
"fr": "Règles",
|
||||||
|
"en": "Rules",
|
||||||
|
},
|
||||||
|
"Skills": {
|
||||||
|
"fr": "Skills",
|
||||||
|
"en": "Skills",
|
||||||
|
},
|
||||||
|
"Deployed commands": {
|
||||||
|
"fr": "Commandes déployées",
|
||||||
|
"en": "Deployed commands",
|
||||||
|
},
|
||||||
|
"Git hooks": {
|
||||||
|
"fr": "Hooks git",
|
||||||
|
"en": "Git hooks",
|
||||||
|
},
|
||||||
|
"up to date": {
|
||||||
|
"fr": "à jour",
|
||||||
|
"en": "up to date",
|
||||||
|
},
|
||||||
|
"redeploy needed": {
|
||||||
|
"fr": "à redéployer",
|
||||||
|
"en": "redeploy needed",
|
||||||
|
},
|
||||||
|
"not in the repository": {
|
||||||
|
"fr": "absent du dépôt",
|
||||||
|
"en": "not in the repository",
|
||||||
|
},
|
||||||
|
"hook installed": {
|
||||||
|
"fr": "installé",
|
||||||
|
"en": "installed",
|
||||||
|
},
|
||||||
|
"hook not installed": {
|
||||||
|
"fr": "non installé",
|
||||||
|
"en": "not installed",
|
||||||
|
},
|
||||||
|
"%s identifying, %s to re-read — see .claude/rules/04-code-conventions.md": {
|
||||||
|
"fr": "%s identifiant, %s à relire — voir .claude/rules/04-code-conventions.md",
|
||||||
|
"en": "%s identifying, %s to re-read — see .claude/rules/04-code-conventions.md",
|
||||||
|
},
|
||||||
|
"do the comments say how the code works, or where it came from": {
|
||||||
|
"fr": "les commentaires disent-ils le fonctionnement, ou d'où ils viennent",
|
||||||
|
"en": "do the comments say how the code works, or where it came from",
|
||||||
|
},
|
||||||
|
"only the files added to the git index": {
|
||||||
|
"fr": "seulement les fichiers ajoutés à l'index git",
|
||||||
|
"en": "only the files added to the git index",
|
||||||
|
},
|
||||||
|
"drop the narrative signals, keep the certain findings": {
|
||||||
|
"fr": "laisser les signaux de récit, garder les trouvailles sûres",
|
||||||
|
"en": "drop the narrative signals, keep the certain findings",
|
||||||
|
},
|
||||||
|
"give a path, or --staged": {
|
||||||
|
"fr": "donner un chemin, ou --staged",
|
||||||
|
"en": "give a path, or --staged",
|
||||||
|
},
|
||||||
"unknown result": {
|
"unknown result": {
|
||||||
"fr": "résultat inconnu",
|
"fr": "résultat inconnu",
|
||||||
"en": "unknown result",
|
"en": "unknown result",
|
||||||
|
|
|
||||||
|
|
@ -1754,7 +1754,7 @@ class TodoUpgrade:
|
||||||
# TODO exécuter next line si status != 0 et log contient
|
# TODO exécuter next line si status != 0 et log contient
|
||||||
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
|
# psycopg2.errors.UndefinedTable: relation "discuss_channel" does not exist
|
||||||
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
|
# LIGNE 1 : SELECT "discuss_channel"."id" FROM "discuss_channel" WHERE (...
|
||||||
# source ./.venv.odoo18.0_python3.12.10/bin/activate && cat script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py | ./odoo18.0/odoo/odoo-bin shell -d ripbylop_stage_prod_17_nov_2025
|
# source ./.venv.odoo18.0_python3.12.10/bin/activate && cat script/postgresql/migration/fix_migration_postgresql_17_to_postgresql_18_module_mail_nov_2025.py | ./odoo18.0/odoo/odoo-bin shell -d riplop_stage_prod_17_nov_2025
|
||||||
# psycopg2.errors.ForeignKeyViolation: insert or update on table "discuss_channel_member" violates foreign key constraint "discuss_channel_member_channel_id_fkey"
|
# psycopg2.errors.ForeignKeyViolation: insert or update on table "discuss_channel_member" violates foreign key constraint "discuss_channel_member_channel_id_fkey"
|
||||||
# DÉTAIL : Key (channel_id)=(20) is not present in table "discuss_channel".
|
# DÉTAIL : Key (channel_id)=(20) is not present in table "discuss_channel".
|
||||||
# ./script/database/migrate/process_backup_file.py --path_backup_zip image_db/db.zip --path_output_zip image_db/dbFIX.zip --word_to_delete discuss_channel_channel_type_not_null
|
# ./script/database/migrate/process_backup_file.py --path_backup_zip image_db/db.zip --path_output_zip image_db/dbFIX.zip --word_to_delete discuss_channel_channel_type_not_null
|
||||||
|
|
|
||||||
386
test/test_check_comment_hygiene.py
Normal file
386
test/test_check_comment_hygiene.py
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||||
|
"""L'outil voit-il ce qu'il doit, et se tait-il sur le reste ?
|
||||||
|
|
||||||
|
Un outil de style qui crie pour rien se fait ignorer en entier : ces tests
|
||||||
|
pèsent donc les silences autant que les trouvailles. Le présent de l'indicatif
|
||||||
|
(« mesure le temps »), une version Odoo à quatre nombres, la boucle locale et
|
||||||
|
un chemin en gabarit doivent passer sans un mot.
|
||||||
|
|
||||||
|
La part qu'aucun motif ne juge — « cette phrase énonce-t-elle un fait durable
|
||||||
|
ou raconte-t-elle une journée » — n'est pas testée : elle n'est pas décidable.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(
|
||||||
|
0, os.path.join(os.path.dirname(__file__), "..", "script", "analyse")
|
||||||
|
)
|
||||||
|
|
||||||
|
import check_comment_hygiene as hygiene # noqa: E402
|
||||||
|
|
||||||
|
OUTIL = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
"..",
|
||||||
|
"script",
|
||||||
|
"analyse",
|
||||||
|
"check_comment_hygiene.py",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def genres(trouvailles):
|
||||||
|
return {f["kind"] for f in trouvailles}
|
||||||
|
|
||||||
|
|
||||||
|
def motifs(trouvailles):
|
||||||
|
return {f["pattern"] for f in trouvailles}
|
||||||
|
|
||||||
|
|
||||||
|
class TestLesAdresses(unittest.TestCase):
|
||||||
|
"""Quatre nombres séparés par des points ne font pas une machine."""
|
||||||
|
|
||||||
|
def test_une_adresse_de_machine(self):
|
||||||
|
self.assertTrue(hygiene.adresse_de_machine("172.31.7.42"))
|
||||||
|
self.assertTrue(hygiene.adresse_de_machine("172.20.4.9"))
|
||||||
|
|
||||||
|
def test_une_version_odoo(self):
|
||||||
|
for version in ("18.0.1.3", "17.0.1.0", "12.0.2.1"):
|
||||||
|
self.assertFalse(hygiene.adresse_de_machine(version), version)
|
||||||
|
|
||||||
|
def test_la_boucle_locale_et_les_masques(self):
|
||||||
|
for valeur in ("127.0.0.1", "127.0.1.1", "0.0.0.0", "255.255.255.255"):
|
||||||
|
self.assertFalse(hygiene.adresse_de_machine(valeur), valeur)
|
||||||
|
|
||||||
|
def test_une_adresse_de_reseau(self):
|
||||||
|
"""Un dernier octet nul nomme une plage, pas un hôte."""
|
||||||
|
self.assertFalse(hygiene.adresse_de_machine("192.168.122.0"))
|
||||||
|
|
||||||
|
def test_les_blocs_documentaires(self):
|
||||||
|
self.assertFalse(hygiene.adresse_de_machine("192.0.2.5"))
|
||||||
|
self.assertFalse(hygiene.adresse_de_machine("203.0.113.9"))
|
||||||
|
|
||||||
|
def test_un_octet_hors_bornes(self):
|
||||||
|
self.assertFalse(hygiene.adresse_de_machine("999.1.1.1"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestLeTemoignage(unittest.TestCase):
|
||||||
|
"""L'accent sépare le passé qui témoigne du présent qui décrit."""
|
||||||
|
|
||||||
|
def _recits(self, texte):
|
||||||
|
return hygiene.recits(texte)
|
||||||
|
|
||||||
|
def test_le_participe_passe(self):
|
||||||
|
for phrase in (
|
||||||
|
"Vécu sur une machine du parc.",
|
||||||
|
"Mesuré : trois secondes.",
|
||||||
|
"Vécu, sur la base intermédiaire.",
|
||||||
|
"Rapporté au premier essai.",
|
||||||
|
"Mesuré — deux fois de suite.",
|
||||||
|
):
|
||||||
|
self.assertTrue(self._recits(phrase), phrase)
|
||||||
|
|
||||||
|
def test_le_present_de_lindicatif(self):
|
||||||
|
"""« mesure le temps » dit ce que le code fait : rien à signaler."""
|
||||||
|
for phrase in (
|
||||||
|
"La sonde mesure le temps de réponse.",
|
||||||
|
"Le pilote signale au menu que l'étape est finie.",
|
||||||
|
"L'écran constate au démarrage que le service répond.",
|
||||||
|
):
|
||||||
|
self.assertEqual([], self._recits(phrase), phrase)
|
||||||
|
|
||||||
|
def test_une_date_absolue(self):
|
||||||
|
self.assertIn(
|
||||||
|
"date", {t[0] for t in self._recits("Relevé le 2026-08-12.")}
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"date", {t[0] for t in self._recits("Le 24 août 2026, la VM.")}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_la_premiere_personne(self):
|
||||||
|
for phrase in (
|
||||||
|
"Ma conclusion était fausse.",
|
||||||
|
"j'avais écrit le contraire.",
|
||||||
|
):
|
||||||
|
self.assertIn(
|
||||||
|
"personne", {t[0] for t in self._recits(phrase)}, phrase
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_le_nom_releve_nest_pas_le_verbe(self):
|
||||||
|
"""« dans le relevé du serveur » nomme une chose, ne témoigne pas."""
|
||||||
|
for phrase in (
|
||||||
|
"Dans le relevé du serveur, la valeur est vide.",
|
||||||
|
"Le constaté au démarrage sert de référence.",
|
||||||
|
):
|
||||||
|
self.assertEqual([], self._recits(phrase), phrase)
|
||||||
|
|
||||||
|
def test_hier_ne_se_trouve_pas_dans_hierarchie(self):
|
||||||
|
for phrase in (
|
||||||
|
"La hiérarchie des modèles.",
|
||||||
|
"The hierarchy of models.",
|
||||||
|
):
|
||||||
|
self.assertEqual([], self._recits(phrase), phrase)
|
||||||
|
|
||||||
|
def test_reproduit_est_aussi_du_present(self):
|
||||||
|
"""Présent et participe s'écrivent pareil : le motif ne tranche pas."""
|
||||||
|
self.assertEqual([], self._recits("Le pilote reproduit la config."))
|
||||||
|
|
||||||
|
def test_toutes_les_occurrences_dun_bloc(self):
|
||||||
|
"""Un bloc qui répète le marqueur ne cache pas le reste du travail."""
|
||||||
|
trouves = self._recits("Mesuré sur la base. Puis vécu sur la copie.")
|
||||||
|
self.assertEqual(2, len({t[1].lower() for t in trouves}))
|
||||||
|
|
||||||
|
|
||||||
|
class TestLesIdentifiants(unittest.TestCase):
|
||||||
|
def test_un_courriel(self):
|
||||||
|
self.assertIn(
|
||||||
|
"courriel", {t[0] for t in hygiene.identifiants("a@exemple.ca")}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_le_courriel_du_proprietaire_passe(self):
|
||||||
|
self.assertEqual([], hygiene.identifiants("contact@technolibre.ca"))
|
||||||
|
|
||||||
|
def test_un_chemin_de_compte(self):
|
||||||
|
self.assertIn(
|
||||||
|
"compte",
|
||||||
|
{t[0] for t in hygiene.identifiants("/home/quelquun/git/")},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_un_chemin_en_gabarit_passe(self):
|
||||||
|
for chemin in ("/home/<utilisateur>/git/", "/home/$USER/git/"):
|
||||||
|
self.assertEqual([], hygiene.identifiants(chemin), chemin)
|
||||||
|
|
||||||
|
def test_la_liste_privee(self):
|
||||||
|
trouves = hygiene.identifiants(
|
||||||
|
"migration de AcmeCorp", termes=["acmecorp"]
|
||||||
|
)
|
||||||
|
self.assertEqual([("nom privé", "acmecorp", 13)], trouves)
|
||||||
|
|
||||||
|
def test_sans_liste_privee_rien_nest_refuse(self):
|
||||||
|
self.assertEqual([], hygiene.identifiants("migration de AcmeCorp"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCeQuiEstLu(unittest.TestCase):
|
||||||
|
"""Les commentaires et les docstrings, et rien d'autre du code."""
|
||||||
|
|
||||||
|
def test_une_docstring_de_module(self):
|
||||||
|
source = '"""Vécu sur une machine du parc."""\n\n\nX = 1\n'
|
||||||
|
trouvailles = hygiene.inspect("x.py", source=source, termes=[])
|
||||||
|
self.assertEqual({"récit"}, genres(trouvailles))
|
||||||
|
|
||||||
|
def test_une_docstring_de_fonction(self):
|
||||||
|
source = 'def f():\n """Mesuré sur la copie."""\n return 1\n'
|
||||||
|
trouvailles = hygiene.inspect("x.py", source=source, termes=[])
|
||||||
|
self.assertEqual(2, trouvailles[0]["line"])
|
||||||
|
|
||||||
|
def test_une_chaine_de_code_nest_pas_un_commentaire(self):
|
||||||
|
"""Seule la PREMIÈRE expression d'une portée est une docstring."""
|
||||||
|
source = 'def f():\n return "Vécu sur la copie"\n'
|
||||||
|
self.assertEqual([], hygiene.inspect("x.py", source=source, termes=[]))
|
||||||
|
|
||||||
|
def test_les_lignes_consecutives_forment_un_bloc(self):
|
||||||
|
"""Une phrase coupée en deux lignes reste une phrase."""
|
||||||
|
source = (
|
||||||
|
"# une raison, puis mesuré\n# sur la copie de la base\nX = 1\n"
|
||||||
|
)
|
||||||
|
trouvailles = hygiene.inspect("x.py", source=source, termes=[])
|
||||||
|
self.assertEqual(1, len(trouvailles))
|
||||||
|
|
||||||
|
def test_un_trou_separe_deux_blocs(self):
|
||||||
|
source = "# mesuré sur la copie\nX = 1\n# vécu sur la copie\nY = 2\n"
|
||||||
|
trouvailles = hygiene.inspect("x.py", source=source, termes=[])
|
||||||
|
self.assertEqual([1, 3], [f["line"] for f in trouvailles])
|
||||||
|
|
||||||
|
def test_la_ligne_pointee_est_celle_du_marqueur(self):
|
||||||
|
"""Pointer le début d'un bloc de vingt lignes ne guide personne."""
|
||||||
|
source = "# une raison\n# une autre\n# vécu sur la copie\nX = 1\n"
|
||||||
|
trouvailles = hygiene.inspect("x.py", source=source, termes=[])
|
||||||
|
self.assertEqual([3], [f["line"] for f in trouvailles])
|
||||||
|
|
||||||
|
def test_un_source_illisible_se_replie_sur_les_lignes(self):
|
||||||
|
"""Rendre un rapport vide dirait « propre » d'un fichier non lu."""
|
||||||
|
source = "def f(:\n # vécu sur la copie\n"
|
||||||
|
trouvailles = hygiene.inspect("x.py", source=source, termes=[])
|
||||||
|
self.assertEqual([2], [f["line"] for f in trouvailles])
|
||||||
|
|
||||||
|
def test_un_mot_qui_en_contient_un_autre_ne_pointe_rien(self):
|
||||||
|
"""« je » vit dans « sujet » et « projeté » : pas une trouvaille."""
|
||||||
|
source = (
|
||||||
|
"# Le sujet est la decision, pas la connexion. Avec une ABI\n"
|
||||||
|
"# injectee, rien ne bouge. Le trajet reste projete.\n"
|
||||||
|
"# Ici seulement je regarde le resultat.\n"
|
||||||
|
"X = 1\n"
|
||||||
|
)
|
||||||
|
trouvailles = hygiene.inspect("x.py", source=source, termes=[])
|
||||||
|
self.assertEqual([3], [f["line"] for f in trouvailles])
|
||||||
|
|
||||||
|
def test_un_commentaire_shell_de_fin_de_ligne(self):
|
||||||
|
source = "#!/bin/sh\nrsync -a src dst # copie vers 10.0.0.42\n"
|
||||||
|
trouvailles = hygiene.inspect("x.sh", source=source, termes=[])
|
||||||
|
self.assertEqual(
|
||||||
|
[(2, "adresse")], [(f["line"], f["pattern"]) for f in trouvailles]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_un_diese_entre_guillemets_nouvre_rien(self):
|
||||||
|
source = '#!/bin/sh\necho "# vécu sur la copie"\n'
|
||||||
|
self.assertEqual([], hygiene.inspect("x.sh", source=source, termes=[]))
|
||||||
|
|
||||||
|
def test_un_diese_colle_a_un_mot_nouvre_rien(self):
|
||||||
|
"""`${VAR#prefixe}` et une URL à ancre ne sont pas des commentaires."""
|
||||||
|
source = "#!/bin/sh\necho ${CHEMIN#vécu sur la copie}\n"
|
||||||
|
self.assertEqual([], hygiene.inspect("x.sh", source=source, termes=[]))
|
||||||
|
|
||||||
|
def test_un_script_shell(self):
|
||||||
|
source = "#!/bin/bash\n# vécu sur la copie\necho ok\n"
|
||||||
|
trouvailles = hygiene.inspect("x.sh", source=source, termes=[])
|
||||||
|
self.assertEqual([2], [f["line"] for f in trouvailles])
|
||||||
|
|
||||||
|
def test_le_shebang_nest_pas_un_commentaire(self):
|
||||||
|
self.assertEqual(
|
||||||
|
[],
|
||||||
|
hygiene.inspect(
|
||||||
|
"x.sh", source="#!/bin/bash\necho ok\n", termes=[]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLePerimetre(unittest.TestCase):
|
||||||
|
def test_le_code_tiers_est_hors_perimetre(self):
|
||||||
|
for chemin in (
|
||||||
|
"script/OCA_maintainer-tools/tools/x.py",
|
||||||
|
"addons/quelque_chose/models/x.py",
|
||||||
|
".venv.erplibre/lib/x.py",
|
||||||
|
):
|
||||||
|
self.assertFalse(hygiene.a_balayer(chemin), chemin)
|
||||||
|
|
||||||
|
def test_le_code_du_depot_est_dans_le_perimetre(self):
|
||||||
|
for chemin in (
|
||||||
|
"script/todo/todo.py",
|
||||||
|
"test/test_x.py",
|
||||||
|
"long_test/x.py",
|
||||||
|
):
|
||||||
|
self.assertTrue(hygiene.a_balayer(chemin), chemin)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLaLigneDeCommande(unittest.TestCase):
|
||||||
|
"""Le module peut être juste et le programme faux : on l'exécute."""
|
||||||
|
|
||||||
|
def _lancer(self, source, suffixe=".py", args=()):
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w", suffix=suffixe, delete=False, encoding="utf-8"
|
||||||
|
) as fh:
|
||||||
|
fh.write(source)
|
||||||
|
chemin = fh.name
|
||||||
|
try:
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, OUTIL, chemin, "--no-color", *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
os.unlink(chemin)
|
||||||
|
|
||||||
|
def test_zero_quand_il_ny_a_rien_a_signaler(self):
|
||||||
|
r = self._lancer("# la sonde mesure le temps\nX = 1\n")
|
||||||
|
self.assertEqual(0, r.returncode, r.stdout + r.stderr)
|
||||||
|
self.assertEqual("", r.stdout.strip())
|
||||||
|
|
||||||
|
def test_un_quand_il_y_a_des_trouvailles(self):
|
||||||
|
"""0 rien à signaler, 1 des trouvailles, 2 l'outil a échoué."""
|
||||||
|
r = self._lancer("# vécu sur la copie\nX = 1\n")
|
||||||
|
self.assertEqual(1, r.returncode)
|
||||||
|
self.assertIn("témoignage", r.stdout)
|
||||||
|
|
||||||
|
def test_le_json_porte_le_compte_et_les_trouvailles(self):
|
||||||
|
import json
|
||||||
|
|
||||||
|
r = self._lancer("# vécu sur la copie\nX = 1\n", args=("--json",))
|
||||||
|
rendu = json.loads(r.stdout)
|
||||||
|
self.assertEqual(1, rendu["scanned"])
|
||||||
|
self.assertEqual(1, len(rendu["findings"]))
|
||||||
|
|
||||||
|
def test_identifying_only_laisse_le_recit_dehors(self):
|
||||||
|
r = self._lancer(
|
||||||
|
"# vécu sur la copie\nX = 1\n", args=("--identifying-only",)
|
||||||
|
)
|
||||||
|
self.assertEqual(0, r.returncode)
|
||||||
|
|
||||||
|
def test_sans_chemin_il_le_dit(self):
|
||||||
|
r = subprocess.run(
|
||||||
|
[sys.executable, OUTIL], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
self.assertEqual(2, r.returncode)
|
||||||
|
|
||||||
|
|
||||||
|
class TestLeHook(unittest.TestCase):
|
||||||
|
"""Le hook informe : il ne bloque jamais un commit sur du style."""
|
||||||
|
|
||||||
|
HOOK = os.path.join(
|
||||||
|
os.path.dirname(__file__), "..", "script", "git", "hooks", "pre-commit"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _depot_jetable(self, contenu):
|
||||||
|
"""Un dépôt git neuf, l'outil et le hook posés où le hook les cherche."""
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
racine = tempfile.mkdtemp()
|
||||||
|
self.addCleanup(shutil.rmtree, racine, True)
|
||||||
|
for relatif in ("script/analyse", "script/git/hooks"):
|
||||||
|
os.makedirs(os.path.join(racine, relatif))
|
||||||
|
shutil.copy(OUTIL, os.path.join(racine, "script", "analyse"))
|
||||||
|
shutil.copy(
|
||||||
|
os.path.join(os.path.dirname(OUTIL), "..", "lib_identifiant.py"),
|
||||||
|
os.path.join(racine, "script"),
|
||||||
|
)
|
||||||
|
shutil.copy(self.HOOK, os.path.join(racine, "script", "git", "hooks"))
|
||||||
|
chemin = os.path.join(racine, "exemple.py")
|
||||||
|
with open(chemin, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(contenu)
|
||||||
|
for commande in (
|
||||||
|
["git", "init", "-q"],
|
||||||
|
["git", "add", "exemple.py"],
|
||||||
|
):
|
||||||
|
subprocess.run(
|
||||||
|
commande, cwd=racine, check=True, capture_output=True
|
||||||
|
)
|
||||||
|
return racine
|
||||||
|
|
||||||
|
def _lancer_dans(self, racine):
|
||||||
|
return subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
os.path.join(racine, "script/git/hooks/pre-commit"),
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=racine,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_il_rapporte_ce_qui_est_indexe(self):
|
||||||
|
racine = self._depot_jetable(
|
||||||
|
"# vécu sur la copie, en 10.0.0.42\nX = 1\n"
|
||||||
|
)
|
||||||
|
r = self._lancer_dans(racine)
|
||||||
|
self.assertEqual(0, r.returncode, r.stderr)
|
||||||
|
self.assertIn("exemple.py", r.stderr)
|
||||||
|
self.assertIn("10.0.0.42", r.stderr)
|
||||||
|
self.assertIn("PAS bloqué", r.stderr)
|
||||||
|
|
||||||
|
def test_il_se_tait_sur_un_fichier_propre(self):
|
||||||
|
racine = self._depot_jetable('"""Rend le code de sortie."""\nX = 1\n')
|
||||||
|
r = self._lancer_dans(racine)
|
||||||
|
self.assertEqual(0, r.returncode)
|
||||||
|
self.assertEqual("", r.stderr.strip())
|
||||||
|
|
||||||
|
def test_il_sort_toujours_en_zero(self):
|
||||||
|
r = subprocess.run(
|
||||||
|
[sys.executable, self.HOOK], capture_output=True, text=True
|
||||||
|
)
|
||||||
|
self.assertEqual(0, r.returncode, r.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
|
|
@ -26,7 +26,7 @@ class TestWhatANameMayBe(unittest.TestCase):
|
||||||
"""Le nom entre dans du SQL par `database_identifier` : il se filtre."""
|
"""Le nom entre dans du SQL par `database_identifier` : il se filtre."""
|
||||||
|
|
||||||
def test_ordinary_names_pass(self):
|
def test_ordinary_names_pass(self):
|
||||||
for nom in ("chezlepro", "el_essai", "a", "base-2024", "_interne"):
|
for nom in ("sireine", "el_essai", "a", "base-2024", "_interne"):
|
||||||
self.assertTrue(dup.nom_valide(nom), nom)
|
self.assertTrue(dup.nom_valide(nom), nom)
|
||||||
|
|
||||||
def test_a_name_that_could_carry_sql_is_refused(self):
|
def test_a_name_that_could_carry_sql_is_refused(self):
|
||||||
|
|
|
||||||
|
|
@ -164,9 +164,9 @@ class TestLeCorps(unittest.TestCase):
|
||||||
self.assertEqual([], check(_message(corps)))
|
self.assertEqual([], check(_message(corps)))
|
||||||
|
|
||||||
def test_une_adresse_ip(self):
|
def test_une_adresse_ip(self):
|
||||||
problemes = check(_message("La VM répondait en 192.168.123.170."))
|
problemes = check(_message("La VM répondait en 172.31.7.42."))
|
||||||
self.assertEqual(1, len(problemes))
|
self.assertEqual(1, len(problemes))
|
||||||
self.assertIn("192.168.123.170", problemes[0])
|
self.assertIn("172.31.7.42", problemes[0])
|
||||||
|
|
||||||
def test_les_adresses_sans_porteur_passent(self):
|
def test_les_adresses_sans_porteur_passent(self):
|
||||||
"""0.0.0.0 et 127.0.0.1 ne désignent aucune machine du parc."""
|
"""0.0.0.0 et 127.0.0.1 ne désignent aucune machine du parc."""
|
||||||
|
|
@ -187,7 +187,7 @@ class TestLeCorps(unittest.TestCase):
|
||||||
|
|
||||||
def test_un_chemin_de_compte(self):
|
def test_un_chemin_de_compte(self):
|
||||||
problemes = check(
|
problemes = check(
|
||||||
_message("Le venv vit dans /home/mathieu/git/erplibre/.")
|
_message("Le venv vit dans /home/sireine/git/erplibre/.")
|
||||||
)
|
)
|
||||||
self.assertEqual(1, len(problemes))
|
self.assertEqual(1, len(problemes))
|
||||||
self.assertIn("chemin de compte", problemes[0])
|
self.assertIn("chemin de compte", problemes[0])
|
||||||
|
|
@ -221,7 +221,7 @@ class TestLeCorps(unittest.TestCase):
|
||||||
try:
|
try:
|
||||||
problemes = check(_message("Migration de AcmeCorp, six paliers."))
|
problemes = check(_message("Migration de AcmeCorp, six paliers."))
|
||||||
self.assertEqual(1, len(problemes))
|
self.assertEqual(1, len(problemes))
|
||||||
self.assertIn("liste privée", problemes[0])
|
self.assertIn("nom refusé", problemes[0])
|
||||||
finally:
|
finally:
|
||||||
os.unlink(commit_msg_lib.NOMS_INTERDITS)
|
os.unlink(commit_msg_lib.NOMS_INTERDITS)
|
||||||
commit_msg_lib.NOMS_INTERDITS = origine
|
commit_msg_lib.NOMS_INTERDITS = origine
|
||||||
|
|
@ -264,7 +264,7 @@ class TestLeCorps(unittest.TestCase):
|
||||||
try:
|
try:
|
||||||
problemes = check(_message("Une raison.\n\nRefs: acmecorp-42"))
|
problemes = check(_message("Une raison.\n\nRefs: acmecorp-42"))
|
||||||
self.assertEqual(1, len(problemes))
|
self.assertEqual(1, len(problemes))
|
||||||
self.assertIn("liste privée", problemes[0])
|
self.assertIn("nom refusé", problemes[0])
|
||||||
finally:
|
finally:
|
||||||
os.unlink(commit_msg_lib.NOMS_INTERDITS)
|
os.unlink(commit_msg_lib.NOMS_INTERDITS)
|
||||||
commit_msg_lib.NOMS_INTERDITS = origine
|
commit_msg_lib.NOMS_INTERDITS = origine
|
||||||
|
|
|
||||||
|
|
@ -667,7 +667,7 @@ class TestTheVerdictsSection(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_a_plain_name_is_its_own_lineage(self):
|
def test_a_plain_name_is_its_own_lineage(self):
|
||||||
self.assertEqual("copy_chezlepro3", residue.famille("copy_chezlepro3"))
|
self.assertEqual("copy_sireine3", residue.famille("copy_sireine3"))
|
||||||
|
|
||||||
def test_another_migration_verdicts_are_not_shown(self):
|
def test_another_migration_verdicts_are_not_shown(self):
|
||||||
# Deux migrations partagent le fichier. Attribuer l'échec de
|
# Deux migrations partagent le fichier. Attribuer l'échec de
|
||||||
|
|
|
||||||
|
|
@ -257,7 +257,7 @@ class TestTheDatabaseNameTheFileSuggests(unittest.TestCase):
|
||||||
return database_name_from_file(chemin, **kw)
|
return database_name_from_file(chemin, **kw)
|
||||||
|
|
||||||
def test_the_zip_extension_goes_away(self):
|
def test_the_zip_extension_goes_away(self):
|
||||||
self.assertEqual("chezlepro3", self.nom("image_db/chezlepro3.zip"))
|
self.assertEqual("sireine3", self.nom("image_db/sireine3.zip"))
|
||||||
|
|
||||||
def test_an_uppercase_extension_goes_away_too(self):
|
def test_an_uppercase_extension_goes_away_too(self):
|
||||||
self.assertEqual("client", self.nom("image_db/CLIENT.ZIP"))
|
self.assertEqual("client", self.nom("image_db/CLIENT.ZIP"))
|
||||||
|
|
|
||||||
|
|
@ -41,18 +41,18 @@ from script.todo.todo_i18n import t # noqa: E402
|
||||||
CONFIG = """Host *
|
CONFIG = """Host *
|
||||||
ServerAliveInterval 60
|
ServerAliveInterval 60
|
||||||
|
|
||||||
Host novipro_private
|
Host pro_private
|
||||||
HostName 192.168.100.110
|
HostName 192.168.100.110
|
||||||
User mathben
|
User admin
|
||||||
|
|
||||||
Host novipro_private+ERPLibre01
|
Host pro_private+ERPLibre01
|
||||||
HostName 192.168.122.50
|
HostName 192.168.122.50
|
||||||
User mathben
|
User admin
|
||||||
StrictHostKeyChecking no
|
StrictHostKeyChecking no
|
||||||
UserKnownHostsFile /dev/null
|
UserKnownHostsFile /dev/null
|
||||||
IdentityFile /home/erplibre/.ssh/id_ed25519
|
IdentityFile /home/erplibre/.ssh/id_ed25519
|
||||||
IdentitiesOnly yes
|
IdentitiesOnly yes
|
||||||
ProxyJump novipro_private
|
ProxyJump pro_private
|
||||||
|
|
||||||
Host erplibre-ubuntu-2604 erplibre-2604-bis
|
Host erplibre-ubuntu-2604 erplibre-2604-bis
|
||||||
HostName 192.168.123.165
|
HostName 192.168.123.165
|
||||||
|
|
@ -63,11 +63,11 @@ Host web-?
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sortie de « ssh -G » pour l'alias à « + », réduite à ce qui compte.
|
# Sortie de « ssh -G » pour l'alias à « + », réduite à ce qui compte.
|
||||||
SSH_G = """host novipro_private+erplibre01
|
SSH_G = """host pro_private+erplibre01
|
||||||
hostname 192.168.122.50
|
hostname 192.168.122.50
|
||||||
user mathben
|
user admin
|
||||||
port 22
|
port 22
|
||||||
proxyjump novipro_private
|
proxyjump pro_private
|
||||||
identityfile /home/erplibre/.ssh/id_ed25519
|
identityfile /home/erplibre/.ssh/id_ed25519
|
||||||
identityfile ~/.ssh/id_rsa
|
identityfile ~/.ssh/id_rsa
|
||||||
identitiesonly yes
|
identitiesonly yes
|
||||||
|
|
@ -93,8 +93,8 @@ class TestLectureConfig(unittest.TestCase):
|
||||||
def test_it_reads_hosts_in_file_order(self):
|
def test_it_reads_hosts_in_file_order(self):
|
||||||
hosts = TODO._ssh_config_entries(self.chemin)
|
hosts = TODO._ssh_config_entries(self.chemin)
|
||||||
noms = [n for n, _i in hosts]
|
noms = [n for n, _i in hosts]
|
||||||
self.assertEqual("novipro_private", noms[0])
|
self.assertEqual("pro_private", noms[0])
|
||||||
self.assertIn("novipro_private+ERPLibre01", noms)
|
self.assertIn("pro_private+ERPLibre01", noms)
|
||||||
|
|
||||||
def test_a_host_line_with_two_patterns_gives_two_aliases(self):
|
def test_a_host_line_with_two_patterns_gives_two_aliases(self):
|
||||||
"""C'est ce que le générateur du dépôt écrit (« Host {' '.join(names)} »).
|
"""C'est ce que le générateur du dépôt écrit (« Host {' '.join(names)} »).
|
||||||
|
|
@ -119,14 +119,14 @@ class TestLectureConfig(unittest.TestCase):
|
||||||
|
|
||||||
def test_the_plus_alias_stays_one_name(self):
|
def test_the_plus_alias_stays_one_name(self):
|
||||||
noms = [n for n, _i in TODO._ssh_config_entries(self.chemin)]
|
noms = [n for n, _i in TODO._ssh_config_entries(self.chemin)]
|
||||||
self.assertNotIn("novipro_private", noms[1:2] and [])
|
self.assertNotIn("pro_private", noms[1:2] and [])
|
||||||
self.assertIn("novipro_private+ERPLibre01", noms)
|
self.assertIn("pro_private+ERPLibre01", noms)
|
||||||
|
|
||||||
def test_hostname_and_user_are_kept(self):
|
def test_hostname_and_user_are_kept(self):
|
||||||
hosts = dict(TODO._ssh_config_entries(self.chemin))
|
hosts = dict(TODO._ssh_config_entries(self.chemin))
|
||||||
info = hosts["novipro_private+ERPLibre01"]
|
info = hosts["pro_private+ERPLibre01"]
|
||||||
self.assertEqual("192.168.122.50", info["hostname"])
|
self.assertEqual("192.168.122.50", info["hostname"])
|
||||||
self.assertEqual("mathben", info["user"])
|
self.assertEqual("admin", info["user"])
|
||||||
|
|
||||||
def test_a_missing_file_is_not_a_crash(self):
|
def test_a_missing_file_is_not_a_crash(self):
|
||||||
self.assertEqual([], TODO._ssh_config_entries("/nexistepas/config"))
|
self.assertEqual([], TODO._ssh_config_entries("/nexistepas/config"))
|
||||||
|
|
@ -138,9 +138,9 @@ class TestResolution(unittest.TestCase):
|
||||||
"subprocess.run",
|
"subprocess.run",
|
||||||
return_value=subprocess.CompletedProcess([], 0, SSH_G, ""),
|
return_value=subprocess.CompletedProcess([], 0, SSH_G, ""),
|
||||||
):
|
):
|
||||||
cfg = TODO._ssh_resolve("novipro_private+ERPLibre01")
|
cfg = TODO._ssh_resolve("pro_private+ERPLibre01")
|
||||||
self.assertEqual("192.168.122.50", cfg["hostname"])
|
self.assertEqual("192.168.122.50", cfg["hostname"])
|
||||||
self.assertEqual("novipro_private", cfg["proxyjump"])
|
self.assertEqual("pro_private", cfg["proxyjump"])
|
||||||
|
|
||||||
def test_the_first_identityfile_wins(self):
|
def test_the_first_identityfile_wins(self):
|
||||||
"""ssh -G les répète toutes ; la première est celle qu'il essaiera."""
|
"""ssh -G les répète toutes ; la première est celle qu'il essaiera."""
|
||||||
|
|
@ -177,9 +177,9 @@ class TestCommandeSshfs(unittest.TestCase):
|
||||||
def _resolue(self):
|
def _resolue(self):
|
||||||
return {
|
return {
|
||||||
"hostname": "192.168.122.50",
|
"hostname": "192.168.122.50",
|
||||||
"user": "mathben",
|
"user": "admin",
|
||||||
"port": "22",
|
"port": "22",
|
||||||
"proxyjump": "novipro_private",
|
"proxyjump": "pro_private",
|
||||||
"identityfile": "/home/erplibre/.ssh/id_ed25519",
|
"identityfile": "/home/erplibre/.ssh/id_ed25519",
|
||||||
"identitiesonly": "yes",
|
"identitiesonly": "yes",
|
||||||
"stricthostkeychecking": "false",
|
"stricthostkeychecking": "false",
|
||||||
|
|
@ -188,10 +188,10 @@ class TestCommandeSshfs(unittest.TestCase):
|
||||||
|
|
||||||
def test_a_plus_alias_becomes_a_resolved_target(self):
|
def test_a_plus_alias_becomes_a_resolved_target(self):
|
||||||
cmd, contourne = self.todo._sshfs_command(
|
cmd, contourne = self.todo._sshfs_command(
|
||||||
"novipro_private+ERPLibre01", "/tmp/mnt", self._resolue()
|
"pro_private+ERPLibre01", "/tmp/mnt", self._resolue()
|
||||||
)
|
)
|
||||||
self.assertTrue(contourne)
|
self.assertTrue(contourne)
|
||||||
self.assertIn("mathben@192.168.122.50:/", cmd)
|
self.assertIn("admin@192.168.122.50:/", cmd)
|
||||||
self.assertNotIn("+", cmd)
|
self.assertNotIn("+", cmd)
|
||||||
|
|
||||||
def test_the_options_that_matter_travel_with_it(self):
|
def test_the_options_that_matter_travel_with_it(self):
|
||||||
|
|
@ -199,7 +199,7 @@ class TestCommandeSshfs(unittest.TestCase):
|
||||||
une IP DHCP recyclée fait échouer le montage sur sa clé d'hôte."""
|
une IP DHCP recyclée fait échouer le montage sur sa clé d'hôte."""
|
||||||
cmd, _ = self.todo._sshfs_command("a+b", "/tmp/mnt", self._resolue())
|
cmd, _ = self.todo._sshfs_command("a+b", "/tmp/mnt", self._resolue())
|
||||||
for attendu in (
|
for attendu in (
|
||||||
"-o ProxyJump=novipro_private",
|
"-o ProxyJump=pro_private",
|
||||||
"-o Port=22",
|
"-o Port=22",
|
||||||
"-o IdentityFile=/home/erplibre/.ssh/id_ed25519",
|
"-o IdentityFile=/home/erplibre/.ssh/id_ed25519",
|
||||||
"-o IdentitiesOnly=yes",
|
"-o IdentitiesOnly=yes",
|
||||||
|
|
@ -250,7 +250,7 @@ class TestDiagnostic(unittest.TestCase):
|
||||||
"nothing listening on the SSH port",
|
"nothing listening on the SSH port",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"mathben@x: Permission denied (publickey).",
|
"admin@x: Permission denied (publickey).",
|
||||||
"authentication refused: check User and key",
|
"authentication refused: check User and key",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
@ -285,8 +285,8 @@ class TestFlux(unittest.TestCase):
|
||||||
todo._ssh_probe = lambda alias, timeout=8: probe
|
todo._ssh_probe = lambda alias, timeout=8: probe
|
||||||
todo._ssh_resolve = lambda alias: {
|
todo._ssh_resolve = lambda alias: {
|
||||||
"hostname": "192.168.122.50",
|
"hostname": "192.168.122.50",
|
||||||
"user": "mathben",
|
"user": "admin",
|
||||||
"proxyjump": "novipro_private",
|
"proxyjump": "pro_private",
|
||||||
}
|
}
|
||||||
return todo
|
return todo
|
||||||
|
|
||||||
|
|
@ -377,11 +377,11 @@ class TestFlux(unittest.TestCase):
|
||||||
def test_the_plus_alias_is_bypassed_before_being_run(self):
|
def test_the_plus_alias_is_bypassed_before_being_run(self):
|
||||||
"""Le vrai correctif : la commande lancée ne contient plus le « + »."""
|
"""Le vrai correctif : la commande lancée ne contient plus le « + »."""
|
||||||
todo = self._todo(0)
|
todo = self._todo(0)
|
||||||
# [1] novipro_private · [2] novipro_private+ERPLibre01 · [3] la VM
|
# [1] pro_private · [2] pro_private+ERPLibre01 · [3] la VM
|
||||||
_s, _c, _r = self._joue(todo, CONFIG, "2")
|
_s, _c, _r = self._joue(todo, CONFIG, "2")
|
||||||
lancee = todo.lances[0]
|
lancee = todo.lances[0]
|
||||||
self.assertIn("mathben@192.168.122.50:/", lancee)
|
self.assertIn("admin@192.168.122.50:/", lancee)
|
||||||
self.assertIn("-o ProxyJump=novipro_private", lancee)
|
self.assertIn("-o ProxyJump=pro_private", lancee)
|
||||||
self.assertNotIn("+ERPLibre01", lancee)
|
self.assertNotIn("+ERPLibre01", lancee)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue