Les tests longs lançaient la CLI sans privilège, alors que le dossier des images appartient à root en 755 : arrêt à l'étape 1 sur « Permission refusée », avant d'avoir rien créé. Ils ne pouvaient donc pas tourner du tout — pas même échouer utilement. Le menu passe sudo depuis toujours ; c'est cette forme qui est reprise, et jamais à blanc. Le refus de place d'un miroir reprenait un champ que tout appelant laisse à zéro : « moins de 0 o libres » n'annonce aucun seuil et ne dit pas ce qui a été mesuré. Treize tests héritaient du plancher de production, 10 Gio, et rougissaient sur un /tmp en tmpfs. --- EN --- The long tests ran the CLI unprivileged, while the image directory is owned by root, mode 755: they stopped at step 1 on "permission denied", before creating anything. They therefore could not run at all — not even fail usefully. The menu has always passed sudo; that is the form taken here, and never on a dry run. A mirror's refusal for lack of space echoed a field every caller leaves at zero: "less than 0 B free" announces no threshold and does not say what was measured. Thirteen tests inherited the production floor, 10 GiB, and went red on a tmpfs /tmp. Assisted-by: Claude Opus 5
564 lines
18 KiB
Go
564 lines
18 KiB
Go
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"net/http/cgi"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDepotDeURL(t *testing.T) {
|
|
cas := []struct {
|
|
brut, depot, reste string
|
|
}{
|
|
{"https://h/o/d.git/info/refs?service=git-upload-pack",
|
|
"https://h/o/d.git", "/info/refs"},
|
|
{"https://h/o/d/info/refs?service=git-upload-pack",
|
|
"https://h/o/d", "/info/refs"},
|
|
{"https://h/o/d.git/git-upload-pack", "https://h/o/d.git",
|
|
"/git-upload-pack"},
|
|
}
|
|
for _, c := range cas {
|
|
u, _ := url.Parse(c.brut)
|
|
depot, reste, ok := DepotDeURL(u)
|
|
if !ok || depot != c.depot || reste != c.reste {
|
|
t.Errorf("%s → (%q, %q, %v)", c.brut, depot, reste, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDepotDeURLRefuse(t *testing.T) {
|
|
for _, brut := range []string{
|
|
"https://h/o/d.git/objects/ab/cd",
|
|
"https://h/info/refs",
|
|
"https://h/miroir/core.db",
|
|
} {
|
|
u, _ := url.Parse(brut)
|
|
if _, _, ok := DepotDeURL(u); ok {
|
|
t.Errorf("%s pris pour une négociation", brut)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Le chemin du miroir porte l'HÔTE : deux forges peuvent servir « /odoo/odoo »,
|
|
// et les confondre donnerait à l'une le contenu de l'autre.
|
|
func TestCheminMiroirSepareLesForges(t *testing.T) {
|
|
g := &GitMirror{Dir: "/var/x"}
|
|
a, err := g.CheminMiroir("https://github.com/odoo/odoo.git")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := g.CheminMiroir("https://autre.example/odoo/odoo.git")
|
|
if a == b {
|
|
t.Error("deux forges partagent un miroir")
|
|
}
|
|
if !strings.HasSuffix(a, ".git") {
|
|
t.Errorf("le miroir n'est pas un dépôt nu : %s", a)
|
|
}
|
|
// « .git » écrit ou non par l'amont donne le même miroir.
|
|
c, _ := g.CheminMiroir("https://github.com/odoo/odoo")
|
|
if a != c {
|
|
t.Errorf("« .git » change le miroir : %s contre %s", a, c)
|
|
}
|
|
}
|
|
|
|
// Un chemin qui remonte écrirait hors de la racine.
|
|
func TestCheminMiroirRefuseCeQuiRemonte(t *testing.T) {
|
|
g := &GitMirror{Dir: "/var/x"}
|
|
for _, brut := range []string{
|
|
"https://h/../../etc/passwd",
|
|
"https://h/o/../../..",
|
|
"https://h/",
|
|
"pas une url",
|
|
} {
|
|
if chemin, err := g.CheminMiroir(brut); err == nil {
|
|
t.Errorf("%s accepté et rendu %q", brut, chemin)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMiroirEteintQuandRienNestConfigure(t *testing.T) {
|
|
if (&GitMirror{}).Actif() {
|
|
t.Error("un miroir sans répertoire se dit actif")
|
|
}
|
|
// Un répertoire sans le programme de git ne sert à rien : mieux vaut le
|
|
// relais vers l'amont, qui fonctionne.
|
|
g := &GitMirror{Dir: "/var/x", Backend: "/nexiste/pas"}
|
|
if g.Actif() {
|
|
t.Error("un miroir sans git-http-backend se dit actif")
|
|
}
|
|
}
|
|
|
|
// amontGit monte un dépôt nu ET le serveur qui le publie, comme le ferait une
|
|
// forge. L'URL rendue est celle qu'un client — ou le miroir — interroge.
|
|
func amontGit(t *testing.T) (string, *httptest.Server) {
|
|
t.Helper()
|
|
if TrouverBackend() == "" {
|
|
t.Skip("git-http-backend absent de cette machine")
|
|
}
|
|
nu := depotDEssai(t)
|
|
racine := filepath.Dir(nu)
|
|
h := &cgi.Handler{
|
|
Path: TrouverBackend(),
|
|
Dir: racine,
|
|
Env: []string{
|
|
"GIT_PROJECT_ROOT=" + racine,
|
|
"GIT_HTTP_EXPORT_ALL=1",
|
|
},
|
|
InheritEnv: []string{"PATH"},
|
|
}
|
|
srv := httptest.NewServer(h)
|
|
t.Cleanup(srv.Close)
|
|
return srv.URL + "/" + filepath.Base(nu), srv
|
|
}
|
|
|
|
// depotDEssai fabrique un dépôt nu local, servi comme s'il était l'amont.
|
|
func depotDEssai(t *testing.T) string {
|
|
t.Helper()
|
|
racine := t.TempDir()
|
|
nu := filepath.Join(racine, "amont.git")
|
|
travail := filepath.Join(racine, "travail")
|
|
lancer := func(dir string, args ...string) {
|
|
t.Helper()
|
|
c := exec.Command("git", args...)
|
|
c.Dir = dir
|
|
c.Env = append(os.Environ(),
|
|
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@e",
|
|
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@e")
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v : %v : %s", args, err, out)
|
|
}
|
|
}
|
|
lancer(racine, "init", "-q", "--bare", "--initial-branch=main", nu)
|
|
lancer(racine, "init", "-q", "--initial-branch=main", travail)
|
|
if err := os.WriteFile(
|
|
filepath.Join(travail, "f.txt"), []byte("bonjour"), 0o644,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lancer(travail, "add", "f.txt")
|
|
lancer(travail, "commit", "-qm", "premier")
|
|
lancer(travail, "push", "-q", nu, "HEAD:refs/heads/main")
|
|
return nu
|
|
}
|
|
|
|
// Le refus de place est la SEULE trace qu'un opérateur verra, et il nommait
|
|
// le champ PlancherLibre — laissé à zéro par tout appelant ordinaire — au
|
|
// lieu du plancher qui avait refusé. « moins de 0 o libres sur le disque »
|
|
// n'est ni vrai ni exploitable : il n'annonce aucun seuil et ne dit pas ce
|
|
// qu'il a mesuré.
|
|
func TestLeRefusConnaitLePlancherQuiARefuse(t *testing.T) {
|
|
g := &GitMirror{Dir: t.TempDir()}
|
|
libres, plancher, assez := g.placeSuffisante()
|
|
if plancher != PlancherParDefaut {
|
|
t.Fatalf("plancher rendu %d, le défaut est %d",
|
|
plancher, PlancherParDefaut)
|
|
}
|
|
if libres < 0 {
|
|
t.Skip("place non mesurable sur ce répertoire")
|
|
}
|
|
if assez != (libres > plancher) {
|
|
t.Fatalf("verdict %v pour %d libres sous un plancher de %d",
|
|
assez, libres, plancher)
|
|
}
|
|
}
|
|
|
|
// Une mesure impossible ne bloque pas — c'est le choix déjà écrit — mais elle
|
|
// doit se DISTINGUER d'un disque vide : zéro octet libre est une mesure, pas
|
|
// une absence de mesure.
|
|
func TestUnePlaceNonMesurableNeBloquePasEtSeDistingue(t *testing.T) {
|
|
g := &GitMirror{Dir: filepath.Join(t.TempDir(), "nexiste", "pas")}
|
|
libres, _, assez := g.placeSuffisante()
|
|
if !assez {
|
|
t.Fatal("une mesure impossible ne doit pas refuser le miroir")
|
|
}
|
|
if libres != -1 {
|
|
t.Fatalf("libres %d : une mesure absente doit se distinguer", libres)
|
|
}
|
|
}
|
|
|
|
// Le plancher des tests qui ne portent PAS sur le plancher.
|
|
//
|
|
// Le défaut de production — 10 Gio laissés au disque de l'orchestrateur — n'a
|
|
// rien à voir avec un miroir de trois commits dans un répertoire temporaire,
|
|
// et il fait échouer treize tests sur toute machine dont le répertoire
|
|
// temporaire offre moins que cela : un /tmp en tmpfs y suffit. Le refus était
|
|
// alors pris pour une panne du miroir.
|
|
//
|
|
// Le plancher lui-même est éprouvé plus bas, avec une valeur qu'aucun disque
|
|
// n'atteint.
|
|
const plancherDEssai int64 = 1
|
|
|
|
// L'épreuve qui compte : un client clone à travers le miroir, sans que rien
|
|
// dans l'invité soit configuré, et le dépôt arrive complet.
|
|
func TestClonerAuTraversDuMiroir(t *testing.T) {
|
|
if TrouverBackend() == "" {
|
|
t.Skip("git-http-backend absent de cette machine")
|
|
}
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 2 * time.Minute}
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
u := &url.URL{Path: r.URL.Path, RawQuery: r.URL.RawQuery}
|
|
_, reste, ok := DepotDeURL(u)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
// Tout chemin demandé désigne le dépôt qui joue l'amont : ce test
|
|
// mesure le miroir, pas le routage.
|
|
chemin, pret := g.Assurer(r.Context(), amont)
|
|
if !pret {
|
|
http.Error(w, "miroir indisponible", 502)
|
|
return
|
|
}
|
|
g.Servir(w, r, chemin, reste)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
for essai := 1; essai <= 2; essai++ {
|
|
dest := filepath.Join(t.TempDir(), "copie")
|
|
c := exec.Command("git", "clone", "-q", srv.URL+"/essai.git", dest)
|
|
c.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("clone %d : %v : %s", essai, err, out)
|
|
}
|
|
contenu, err := os.ReadFile(filepath.Join(dest, "f.txt"))
|
|
if err != nil || string(contenu) != "bonjour" {
|
|
t.Fatalf("clone %d : contenu %q, err %v", essai, contenu, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Un amont muet mais un miroir déjà là : le miroir sert. C'est ce qui rend un
|
|
// déploiement sans réseau possible pour git, ce qu'aucun cache de réponses ne
|
|
// peut faire de ce protocole.
|
|
func TestUnMiroirExistantSertQuandLAmontEstMuet(t *testing.T) {
|
|
amont, srvAmont := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: time.Minute}
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
// L'amont disparaît : le serveur qui le publiait est fermé, exactement ce
|
|
// que voit une VM quand le réseau tombe.
|
|
srvAmont.Close()
|
|
g.Frais = 0 // forcer une tentative de rafraîchissement
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
if !pret {
|
|
t.Error("le miroir refuse de servir alors qu'il existe")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(chemin, "HEAD")); err != nil {
|
|
t.Errorf("le miroir a été effacé : %v", err)
|
|
}
|
|
}
|
|
|
|
// Un amont muet SANS miroir doit rendre faux : l'appelant retombe alors sur le
|
|
// relais, qui donnera au client la vraie erreur du réseau plutôt qu'une erreur
|
|
// inventée ici.
|
|
func TestAucunMiroirEtAucunAmont(t *testing.T) {
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 20 * time.Second}
|
|
chemin, pret := g.Assurer(
|
|
context.Background(), "https://127.0.0.1:1/nexiste/pas.git")
|
|
if pret {
|
|
t.Errorf("se dit prêt avec %q", chemin)
|
|
}
|
|
// Aucun DÉPÔT ne doit rester derrière : un clonage à moitié fait serait
|
|
// pris pour un miroir valide à la requête suivante. Un répertoire vide,
|
|
// lui, ne trompe personne.
|
|
filepath.Walk(g.Dir, func(p string, info os.FileInfo, err error) error {
|
|
if err == nil && info != nil && info.Name() == "HEAD" {
|
|
t.Errorf("un miroir incomplet subsiste : %s", p)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestDepotsDuFichier(t *testing.T) {
|
|
f := filepath.Join(t.TempDir(), "liste")
|
|
contenu := strings.Join([]string{
|
|
"# un commentaire",
|
|
"",
|
|
"https://h/a.git",
|
|
" https://h/b.git ",
|
|
"https://h/a.git", // le même dépôt figure dans plusieurs manifestes
|
|
}, "\n")
|
|
if err := os.WriteFile(f, []byte(contenu), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := DepotsDuFichier(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
attendu := []string{"https://h/a.git", "https://h/b.git"}
|
|
if len(got) != len(attendu) {
|
|
t.Fatalf("%d dépôts, %d attendus : %v", len(got), len(attendu), got)
|
|
}
|
|
for i := range attendu {
|
|
if got[i] != attendu[i] {
|
|
t.Errorf("dépôt %d : %q, attendu %q", i, got[i], attendu[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// Un dépôt qui échoue ne doit pas emporter les autres : sur une liste de
|
|
// trois cents, il y a toujours un dépôt privé, déplacé ou retiré, et tout
|
|
// arrêter pour lui perdrait le travail déjà fait.
|
|
func TestUnDepotEnEchecNEmportePasLesAutres(t *testing.T) {
|
|
bon, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
depots := []string{
|
|
bon,
|
|
"https://127.0.0.1:1/absent.git",
|
|
bon + "-autre",
|
|
}
|
|
reussis, echoues := g.Prefetch(context.Background(), depots, 3, nil)
|
|
if reussis < 1 {
|
|
t.Errorf("%d réussite(s) : le dépôt joignable n'a pas été pris",
|
|
reussis)
|
|
}
|
|
if echoues < 1 {
|
|
t.Error("aucun échec compté alors qu'un dépôt est injoignable")
|
|
}
|
|
if reussis+echoues != len(depots) {
|
|
t.Errorf("%d + %d ne fait pas %d", reussis, echoues, len(depots))
|
|
}
|
|
}
|
|
|
|
// Le pré-remplissage rend le miroir prêt : la machine suivante n'a plus qu'à
|
|
// être servie, ce qui est tout l'objet de l'avance.
|
|
func TestApresPrefetchLeMiroirEstPret(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
if r, e := g.Prefetch(
|
|
context.Background(), []string{amont}, 2, nil,
|
|
); r != 1 || e != 0 {
|
|
t.Fatalf("pré-remplissage : %d réussis, %d échoués", r, e)
|
|
}
|
|
depots, octets := g.Occupation()
|
|
if depots != 1 || octets == 0 {
|
|
t.Errorf("occupation : %d dépôts, %d octets", depots, octets)
|
|
}
|
|
}
|
|
|
|
// Le plancher protège le disque de l'orchestrateur.
|
|
//
|
|
// Un miroir est COMPLET là où « repo sync » clone en profondeur un : le
|
|
// facteur entre les deux est celui de l'historique, et il ne se devine pas.
|
|
// Sans plancher, une seule installation qui tire trois cents dépôts peut
|
|
// remplir le disque de la machine qui héberge toutes les VM.
|
|
func TestLePlancherRefuseUnMiroirDeplus(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
Dir: t.TempDir(),
|
|
Delai: 30 * time.Second,
|
|
// Plus que tout disque n'en offre : aucun miroir NEUF ne doit passer.
|
|
PlancherLibre: 1 << 62,
|
|
}
|
|
if chemin, pret := g.Assurer(context.Background(), amont); pret {
|
|
t.Errorf("un miroir a été créé sous le plancher : %s", chemin)
|
|
}
|
|
if depots, _ := g.Occupation(); depots != 0 {
|
|
t.Errorf("%d dépôt(s) créés malgré le plancher", depots)
|
|
}
|
|
}
|
|
|
|
// Un miroir DÉJÀ tenu continue d'être servi : une mise à jour ne coûte que ce
|
|
// qui a changé, et le refuser priverait de tout ce qui est déjà là.
|
|
func TestLePlancherNEmpechePasDeServirLexistant(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
g.PlancherLibre = 1 << 62
|
|
g.Frais = 0
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Error("un miroir existant est refusé à cause du plancher")
|
|
}
|
|
}
|
|
|
|
// Ce que le miroir sert doit être COMPTÉ.
|
|
//
|
|
// La passerelle CGI écrit directement dans la réponse : le journal notait zéro
|
|
// octet pour tout ce que le miroir servait, et c'est le chemin qui porte
|
|
// l'essentiel du trafic d'une installation. Un outil dont le journal EST la
|
|
// mesure ne peut pas avoir un chemin muet.
|
|
func TestCeQueLeMiroirSertEstCompte(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
if !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
|
|
var pese int64
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
u := &url.URL{Path: r.URL.Path, RawQuery: r.URL.RawQuery}
|
|
_, reste, ok := DepotDeURL(u)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
pese += g.Servir(w, r, chemin, reste)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dest := filepath.Join(t.TempDir(), "copie")
|
|
c := exec.Command("git", "clone", "-q", srv.URL+"/essai.git", dest)
|
|
c.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
|
if out, err := c.CombinedOutput(); err != nil {
|
|
t.Fatalf("clone : %v : %s", err, out)
|
|
}
|
|
if pese <= 0 {
|
|
t.Error("le miroir a servi un clone entier et le journal dirait zéro")
|
|
}
|
|
}
|
|
|
|
// La liste est triée par TAILLE : c'est ce qu'on cherche quand on surveille
|
|
// la place à la main, et trois dépôts font les trois quarts du total.
|
|
func TestLesDepotsSontTriesParTaille(t *testing.T) {
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir()}
|
|
for nom, poids := range map[string]int{
|
|
"h/petit.git": 10, "h/gros.git": 5000, "h/moyen.git": 500,
|
|
} {
|
|
d := filepath.Join(g.Dir, nom)
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(
|
|
filepath.Join(d, "HEAD"), make([]byte, poids), 0o644,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
depots := g.Depots()
|
|
if len(depots) != 3 {
|
|
t.Fatalf("%d dépôts vus", len(depots))
|
|
}
|
|
for i, attendu := range []string{"h/gros", "h/moyen", "h/petit"} {
|
|
if depots[i].Nom != attendu {
|
|
t.Errorf("rang %d : %q, attendu %q", i, depots[i].Nom, attendu)
|
|
}
|
|
}
|
|
if depots[0].Octets <= depots[2].Octets {
|
|
t.Error("les tailles ne sont pas mesurées")
|
|
}
|
|
}
|
|
|
|
// Effacer un miroir est sans danger : il se refait au prochain besoin. Mais
|
|
// l'effacement doit rester DANS les miroirs — un appel mal formé ne doit pas
|
|
// pouvoir emporter autre chose.
|
|
func TestRetirerRefuseCeQuiEstDehors(t *testing.T) {
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir()}
|
|
dehors := filepath.Join(t.TempDir(), "ailleurs.git")
|
|
if err := os.MkdirAll(dehors, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, cible := range []string{dehors, "/etc", g.Dir, g.Dir + "/x"} {
|
|
if err := g.Retirer(cible); err == nil {
|
|
t.Errorf("%s a été accepté", cible)
|
|
}
|
|
}
|
|
if _, err := os.Stat(dehors); err != nil {
|
|
t.Errorf("un répertoire hors des miroirs a été effacé : %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRetirerEffaceLeMiroir(t *testing.T) {
|
|
amont, _ := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai, Dir: t.TempDir(), Delai: 30 * time.Second}
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
if !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
if err := g.Retirer(chemin); err != nil {
|
|
t.Fatalf("effacement : %v", err)
|
|
}
|
|
if depots, _ := g.Occupation(); depots != 0 {
|
|
t.Errorf("%d dépôt(s) subsistent", depots)
|
|
}
|
|
// Et il se refait : c'est ce qui rend l'effacement sans danger.
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Error("le miroir ne se refait pas après effacement")
|
|
}
|
|
}
|
|
|
|
// Un miroir qui existe est déjà servable : quand l'amont ne répond pas, il
|
|
// doit sortir TOUT DE SUITE, pas au terme du délai d'un clonage.
|
|
//
|
|
// Sans ce délai propre, un amont coupé faisait attendre chaque dépôt jusqu'à
|
|
// son terme : pour les trois cents dépôts d'une installation, des heures pour
|
|
// un déploiement que le disque pouvait servir en entier.
|
|
func TestUnRafraichissementNattendPasCommeUnClonage(t *testing.T) {
|
|
amont, srv := amontGit(t)
|
|
g := &GitMirror{
|
|
PlancherLibre: plancherDEssai,
|
|
Dir: t.TempDir(),
|
|
Delai: 30 * time.Minute,
|
|
DelaiMaj: 2 * time.Second,
|
|
}
|
|
if _, pret := g.Assurer(context.Background(), amont); !pret {
|
|
t.Fatal("le miroir n'a pas pu être créé")
|
|
}
|
|
// L'amont devient MUET : il accepte la connexion et ne répond jamais,
|
|
// exactement ce que fait un paquet jeté sans être refusé.
|
|
srv.Close()
|
|
muet, err := net.Listen("tcp", hoteDe(t, amont))
|
|
if err != nil {
|
|
t.Skipf("le port de l'amont n'a pas pu être repris : %v", err)
|
|
}
|
|
defer muet.Close()
|
|
go func() {
|
|
for {
|
|
c, err := muet.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = c // accepté, jamais répondu
|
|
}
|
|
}()
|
|
|
|
g.Frais = 0
|
|
debut := time.Now()
|
|
chemin, pret := g.Assurer(context.Background(), amont)
|
|
ecoule := time.Since(debut)
|
|
if !pret || chemin == "" {
|
|
t.Fatal("le miroir existant n'a pas été servi")
|
|
}
|
|
if ecoule > 20*time.Second {
|
|
t.Errorf("le rafraîchissement a duré %v : le délai du clonage a"+
|
|
" été appliqué", ecoule)
|
|
}
|
|
}
|
|
|
|
func hoteDe(t *testing.T, brut string) string {
|
|
t.Helper()
|
|
u, err := url.Parse(brut)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return u.Host
|
|
}
|