205 lines
6.2 KiB
Go
205 lines
6.2 KiB
Go
|
|
// © 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())
|
||
|
|
}
|
||
|
|
}
|