erplibre/script/qemu_cache/age_test.go

205 lines
6.2 KiB
Go
Raw Normal View History

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
func TestLireGranularite(t *testing.T) {
for _, cas := range []struct {
brut string
attendue Granularite
}{
{"jour", ParJour}, {"day", ParJour}, {"J", ParJour},
{"semaine", ParSemaine}, {"week", ParSemaine}, {" W ", ParSemaine},
{"mois", ParMois}, {"month", ParMois}, {"m", ParMois},
} {
g, err := LireGranularite(cas.brut)
if err != nil || g != cas.attendue {
t.Errorf("%q → %q, %v", cas.brut, g, err)
}
}
if _, err := LireGranularite("trimestre"); err == nil {
t.Error("une granularité inconnue est acceptée")
}
}
// « 30d » n'existe pas dans la bibliothèque, et un opérateur qui nettoie
// raisonne en jours, pas en sept cent vingt heures.
func TestLireDuree(t *testing.T) {
for _, cas := range []struct {
brut string
attendue time.Duration
}{
{"30j", 30 * 24 * time.Hour},
{"7d", 7 * 24 * time.Hour},
{"12h", 12 * time.Hour},
{"90m", 90 * time.Minute},
{"0.5j", 12 * time.Hour},
} {
d, err := LireDuree(cas.brut)
if err != nil || d != cas.attendue {
t.Errorf("%q → %v, %v (attendu %v)", cas.brut, d, err, cas.attendue)
}
}
for _, brut := range []string{"", "3lunes", "-5j", "abc"} {
if d, err := LireDuree(brut); err == nil {
t.Errorf("%q accepté et rendu %v", brut, d)
}
}
}
// La semaine commence le LUNDI : c'est la convention ISO, celle des outils du
// système. La ramener au dimanche donnerait des tranches qui ne correspondent
// à aucun autre relevé de la machine.
func TestLaSemaineCommenceLundi(t *testing.T) {
// Un mercredi, et le dimanche qui le suit.
mercredi := time.Date(2026, 9, 2, 15, 0, 0, 0, time.Local)
dimanche := time.Date(2026, 9, 6, 15, 0, 0, 0, time.Local)
a := debutDeTranche(mercredi, ParSemaine)
b := debutDeTranche(dimanche, ParSemaine)
if !a.Equal(b) {
t.Errorf("mercredi et le dimanche suivant tombent dans deux"+
" semaines : %s contre %s", a, b)
}
if a.Weekday() != time.Monday {
t.Errorf("la semaine commence un %s", a.Weekday())
}
}
// magasinDate pose un objet du magasin avec une date choisie.
func magasinDate(t *testing.T, s *Store, cle string, quand time.Time, n int) {
t.Helper()
meta, corps := s.paths(cle)
if err := os.MkdirAll(filepath.Dir(corps), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(corps, make([]byte, n), 0o644); err != nil {
t.Fatal(err)
}
// La taille est écrite dans le méta : le magasin refuse un corps dont
// elle ne correspond plus, y voyant une écriture interrompue.
if err := os.WriteFile(
meta, []byte(fmt.Sprintf(`{"size":%d}`, n)), 0o644,
); err != nil {
t.Fatal(err)
}
if err := os.Chtimes(corps, quand, quand); err != nil {
t.Fatal(err)
}
}
func TestTranchesGroupentParAge(t *testing.T) {
s := &Store{Dir: t.TempDir()}
maintenant := time.Now()
magasinDate(t, s, "aaaa1111", maintenant, 100)
magasinDate(t, s, "bbbb2222", maintenant, 200)
magasinDate(t, s, "cccc3333", maintenant.AddDate(0, 0, -40), 300)
jours := s.Tranches(ParJour)
if len(jours) != 2 {
t.Fatalf("%d tranches par jour : %+v", len(jours), jours)
}
// Du plus récent au plus ancien : c'est l'ordre de lecture.
if !jours[0].Debut.After(jours[1].Debut) {
t.Error("les tranches ne descendent pas dans le temps")
}
if jours[0].Objets != 2 || jours[0].Octets != 300 {
t.Errorf("tranche du jour : %+v", jours[0])
}
if n := len(s.Tranches(ParMois)); n != 2 {
t.Errorf("%d tranches par mois, deux attendues", n)
}
}
// Le méta part AVEC le corps : un méta orphelin ferait croire à une copie
// présente, et la lecture échouerait au moment de servir.
func TestPurgerEmporteLeMetaAvecLeCorps(t *testing.T) {
s := &Store{Dir: t.TempDir()}
vieux := time.Now().AddDate(0, 0, -40)
magasinDate(t, s, "aaaa1111", vieux, 500)
magasinDate(t, s, "bbbb2222", time.Now(), 700)
n, octets, err := s.Purger(time.Now().AddDate(0, 0, -30))
if err != nil {
t.Fatal(err)
}
if n != 1 || octets != 500 {
t.Fatalf("purge : %d objets, %d octets", n, octets)
}
meta, corps := s.paths("aaaa1111")
for _, f := range []string{meta, corps} {
if _, err := os.Stat(f); !os.IsNotExist(err) {
t.Errorf("%s subsiste", filepath.Base(f))
}
}
// Et le récent est INTACT : une purge par âge ne touche pas au reste.
if _, f, err := s.Get("bbbb2222"); err != nil {
t.Errorf("l'objet récent a été emporté : %v", err)
} else {
f.Close()
}
}
func TestPurgerToutNeLaisseRien(t *testing.T) {
s := &Store{Dir: t.TempDir()}
for i, cle := range []string{"aaaa1111", "bbbb2222", "cccc3333"} {
magasinDate(t, s, cle, time.Now().AddDate(0, 0, -i), 100)
}
if n, _, err := s.Purger(time.Now().Add(time.Minute)); err != nil ||
n != 3 {
t.Fatalf("purge totale : %d objets, %v", n, err)
}
if st, err := s.Stat(); err != nil || st.Objects != 0 {
t.Errorf("il reste %d objets", st.Objects)
}
}
// L'âge d'un objet est celui de son dernier USAGE, pas de son entrée.
//
// Sans cela, « nettoyer ce qui ne sert plus » emporterait un paquet servi tous
// les jours depuis un an, et il faudrait le retélécharger le lendemain — le
// contraire de ce qu'un cache est là pour faire.
func TestServirRajeunitUnObjet(t *testing.T) {
a := nouvelAmont(t, "charge utile")
p := proxyDeTest(t)
chemin := "/arch/core/os/x86_64/bash-5.2-1-x86_64.pkg.tar.zst"
if w := demande(t, p, a.hote(), chemin); w.Code != 200 {
t.Fatalf("premier appel : %d", w.Code)
}
// L'objet est vieilli à la main, comme s'il dormait depuis quarante jours.
var corps string
filepath.Walk(p.Store.Dir, func(q string, i os.FileInfo, e error) error {
if e == nil && i != nil && filepath.Ext(q) == ".body" {
corps = q
}
return nil
})
if corps == "" {
t.Fatal("aucun objet gardé")
}
vieux := time.Now().AddDate(0, 0, -40)
if err := os.Chtimes(corps, vieux, vieux); err != nil {
t.Fatal(err)
}
if w := demande(t, p, a.hote(), chemin); w.Code != 200 {
t.Fatalf("second appel : %d", w.Code)
}
info, err := os.Stat(corps)
if err != nil {
t.Fatal(err)
}
if time.Since(info.ModTime()) > time.Minute {
t.Errorf("l'objet servi garde sa vieille date (%s) : un nettoyage"+
" par âge l'emporterait alors qu'il sert", info.ModTime())
}
}