erplibre/script/qemu_cache/langue_test.go

239 lines
7.2 KiB
Go
Raw Permalink Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"go/ast"
"go/parser"
"go/token"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"testing"
)
// Les tests lisent les messages en français, langue du code source : une
// langue héritée de l'environnement de qui les lance les ferait échouer sur
// une chaîne traduite.
func TestMain(m *testing.M) {
definirLangue(LangueFr)
os.Exit(m.Run())
}
// clesT rend le texte de chaque appel T("…") des fichiers du paquet, tests
// exclus, avec sa position. Un argument qui n'est pas un littéral est une
// erreur : le catalogue ne pourrait pas le traduire.
func clesT(t *testing.T) map[string]string {
t.Helper()
fichiers, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
fs := token.NewFileSet()
cles := map[string]string{}
for _, f := range fichiers {
if strings.HasSuffix(f, "_test.go") {
continue
}
af, err := parser.ParseFile(fs, f, nil, 0)
if err != nil {
t.Fatal(err)
}
ast.Inspect(af, func(n ast.Node) bool {
c, ok := n.(*ast.CallExpr)
if !ok {
return true
}
id, ok := c.Fun.(*ast.Ident)
if !ok || id.Name != "T" || len(c.Args) != 1 {
return true
}
texte, ok := litteralT(c.Args[0])
pos := fs.Position(c.Pos()).String()
if !ok {
// Une conversion de valeur connue — T(string(gran)) — se
// vérifie par son propre test ; tout autre argument non
// littéral est refusé.
if conv, estConv := c.Args[0].(*ast.CallExpr); estConv {
if fn, ok := conv.Fun.(*ast.Ident); ok && fn.Name == "string" {
return true
}
}
t.Errorf("%s : T() sur un texte non littéral", pos)
return true
}
cles[texte] = pos
return true
})
}
return cles
}
func litteralT(e ast.Expr) (string, bool) {
switch v := e.(type) {
case *ast.BasicLit:
if v.Kind != token.STRING {
return "", false
}
s, err := strconv.Unquote(v.Value)
return s, err == nil
case *ast.BinaryExpr:
if v.Op != token.ADD {
return "", false
}
a, ok1 := litteralT(v.X)
b, ok2 := litteralT(v.Y)
return a + b, ok1 && ok2
case *ast.ParenExpr:
return litteralT(v.X)
}
return "", false
}
// Un message enveloppé sans traduction sortirait en français dans un binaire
// réglé en anglais, sans que rien ne le signale.
func TestChaqueMessageATraductionAnglaise(t *testing.T) {
cles := clesT(t)
if len(cles) < 100 {
t.Fatalf("%d appels T() trouvés : le contrôle ne lit pas le code", len(cles))
}
var manquants []string
for texte, pos := range cles {
if _, ok := anglais[texte]; !ok {
manquants = append(manquants, pos+" "+strconv.Quote(texte))
}
}
sort.Strings(manquants)
for _, m := range manquants {
t.Errorf("sans traduction : %s", m)
}
}
// Une entrée qu'aucun appel n'emploie est un message retiré, ou une clé qui
// ne correspond plus au texte réécrit : sa traduction ne sortirait jamais.
func TestAucuneTraductionOrpheline(t *testing.T) {
cles := clesT(t)
// Clés employées par conversion : les granularités affichées.
for _, g := range []Granularite{ParJour, ParSemaine, ParMois} {
cles[string(g)] = "granularité"
}
var orphelines []string
for texte := range anglais {
if _, ok := cles[texte]; !ok {
orphelines = append(orphelines, strconv.Quote(texte))
}
}
sort.Strings(orphelines)
for _, o := range orphelines {
t.Errorf("traduction sans appel : %s", o)
}
}
var verbeFormat = regexp.MustCompile(`%[-+# 0]*(?:\d+|\*)?(?:\.(?:\d+|\*))?[a-zA-Z%]`)
// Une traduction qui perd, ajoute ou déplace un verbe de format décale les
// valeurs : « %d VM » rendu « %s VM » écrit « %!s(int=3) ».
func TestLesTraductionsGardentLeursVerbes(t *testing.T) {
for fr, en := range anglais {
a, b := verbeFormat.FindAllString(fr, -1), verbeFormat.FindAllString(en, -1)
if strings.Join(a, " ") != strings.Join(b, " ") {
t.Errorf("verbes %v ≠ %v pour %q", a, b, fr)
}
}
}
func TestLaLangueSeResoutDansLOrdre(t *testing.T) {
env := func(v string) func(string) string {
return func(k string) string {
if k == "EL_LANG" {
return v
}
return ""
}
}
cas := []struct {
nom string
args []string
elLang string
veut string
}{
{"défaut", nil, "", LangueFr},
{"EL_LANG anglais", nil, "en", LangueEn},
{"EL_LANG régional", nil, "en_CA.UTF-8", LangueEn},
{"langue inconnue", nil, "de", LangueFr},
{"--lang espacé", []string{"--status", "--lang", "en"}, "", LangueEn},
{"-lang=", []string{"-lang=en"}, "", LangueEn},
{"--lang= l'emporte sur EL_LANG", []string{"--lang=fr"}, "en", LangueFr},
{"--lang sans valeur", []string{"--lang"}, "en", LangueEn},
{"après --, ce n'est plus une option", []string{"--", "--lang", "en"}, "", LangueFr},
{"une valeur n'est pas une option", []string{"--age-by", "lang"}, "", LangueFr},
}
for _, c := range cas {
if got := langueDemandee(c.args, env(c.elLang)); got != c.veut {
t.Errorf("%s : %q, attendu %q", c.nom, got, c.veut)
}
}
}
func TestUnMessageSortEnAnglais(t *testing.T) {
defer definirLangue(LangueFr)
definirLangue(LangueEn)
if got := T("écoutes : http %d, tls %d"); got != "listeners: http %d, tls %d" {
t.Errorf("anglais : %q", got)
}
if got := T("texte que le catalogue ignore"); got != "texte que le catalogue ignore" {
t.Errorf("un texte inconnu doit sortir tel quel, pas %q", got)
}
definirLangue(LangueFr)
if got := T("écoutes : http %d, tls %d"); got != "écoutes : http %d, tls %d" {
t.Errorf("français : %q", got)
}
}
// Le Python du menu lit ces libellés de --status et cette phrase du journal
// dans les DEUX langues : une traduction qui s'en écarte le rend aveugle.
func TestLesLibellesLusParLeMenuRestentReconnaissables(t *testing.T) {
attendus := map[string]string{
"autorité : %s\n": "authority",
"empreinte : %s\n": "fingerprint",
"exceptions : aucune\n": "exceptions",
"dépôts git : %d en miroir, %s\n": "git repos",
"tunnel opaque retenu pour %s (%d échec(s)) : %v": "opaque tunnel kept for %s (",
}
for fr, prefixe := range attendus {
if !strings.HasPrefix(anglais[fr], prefixe) {
t.Errorf("%q traduit en %q, qui ne commence plus par %q",
fr, anglais[fr], prefixe)
}
}
}
// L'option doit être DÉCLARÉE : flag.Parse refuse une option inconnue, et
// chaque appel du menu qui la passe échouerait alors avant de rien dire.
// Le binaire est construit et lancé pour de vrai, sur un cache vide.
func TestLeBinaireAccepteLangEtParleAnglais(t *testing.T) {
if _, err := exec.LookPath("go"); err != nil {
t.Skip("go absent du PATH")
}
dir := t.TempDir()
bin := filepath.Join(dir, "cache")
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
t.Fatalf("construction : %v\n%s", err, out)
}
cmd := exec.Command(bin, "--lang", "en", "--status", "--cache-dir", filepath.Join(dir, "vide"),
"--ca-dir", filepath.Join(dir, "ca"))
cmd.Env = append(os.Environ(), "EL_LANG=fr")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("--lang en --status : %v\n%s", err, out)
}
if !strings.Contains(string(out), "directory :") {
t.Errorf("sortie non anglaise malgré --lang en :\n%s", out)
}
}