433 lines
18 KiB
Go
433 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const searchOut = `extra/zsh 5.9.2-1 [installed]
|
|
A very advanced and programmable command interpreter (shell) for UNIX
|
|
extra/ripgrep 15.2.0-1 [installed: 15.1.0-1]
|
|
A search tool
|
|
core/base-devel 1-2 (base-devel)
|
|
Basic tools to build Arch Linux packages
|
|
`
|
|
|
|
func TestSearchReadsHeaderAndDescriptionAndWhatIsInstalled(t *testing.T) {
|
|
f := ParseSearch(searchOut)
|
|
if len(f) != 3 {
|
|
t.Fatalf("%+v", f)
|
|
}
|
|
if f[0].Repository != "extra" || f[0].Name != "zsh" || !f[0].Installed || f[0].InstalledAs != "5.9.2-1" || !strings.HasPrefix(f[0].Description, "A very advanced") {
|
|
t.Fatalf("%+v", f[0])
|
|
}
|
|
if f[1].InstalledAs != "15.1.0-1" || f[1].Version != "15.2.0-1" {
|
|
t.Fatalf("%+v", f[1])
|
|
}
|
|
if f[2].Installed || len(f[2].Groups) != 1 || f[2].Groups[0] != "base-devel" {
|
|
t.Fatalf("%+v", f[2])
|
|
}
|
|
}
|
|
|
|
func TestASearchThatFindsNothingIsEmptyAndAFailureIsAnError(t *testing.T) {
|
|
m := machine(fake(func(c call) Ran { return Ran{Status: 1} }, nil), 1000)
|
|
r, err := m.Search("nothing", 50)
|
|
if err != nil || r["count"] != 0 {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
m = machine(fake(func(c call) Ran { return Ran{Status: 1, Stderr: "error: failed to initialize alpm library\n"} }, nil), 1000)
|
|
if _, err := m.Search("x", 50); err == nil || !strings.Contains(err.Error(), "failed to initialize") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
const infoOut = `Name : zsh
|
|
Version : 5.9.2-1
|
|
Depends On : pcre2 libcap gdbm
|
|
Optional Deps : grml-zsh-config: grml's zsh setup
|
|
zsh-doc: documentation [installed]
|
|
Required By : None
|
|
Install Reason : Explicitly installed
|
|
|
|
`
|
|
|
|
func TestInfoReadsListsAsListsAndFallsBackToTheRepositories(t *testing.T) {
|
|
p := ParseInfo(infoOut)
|
|
if len(p) != 1 {
|
|
t.Fatalf("%v", p)
|
|
}
|
|
if deps := p[0]["Depends On"].([]string); len(deps) != 3 || deps[2] != "gdbm" {
|
|
t.Fatalf("%v", p[0]["Depends On"])
|
|
}
|
|
if opt := p[0]["Optional Deps"].([]string); len(opt) != 2 || !strings.HasPrefix(opt[1], "zsh-doc") {
|
|
t.Fatalf("%v", p[0]["Optional Deps"])
|
|
}
|
|
if req := p[0]["Required By"].([]string); len(req) != 0 {
|
|
t.Fatalf("None is empty: %v", req)
|
|
}
|
|
var calls []call
|
|
m := machine(byLine(map[string]Ran{
|
|
"pacman -Qi -- zsh": {Status: 1, Stderr: "error: package 'zsh' was not found\n"},
|
|
"pacman -Si -- zsh": {Stdout: "Repository : extra\n" + infoOut},
|
|
}, &calls), 1000)
|
|
r, err := m.Info("zsh")
|
|
if err != nil || r["installed"] != false || r["package"].(map[string]any)["Repository"] != "extra" {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
}
|
|
|
|
func TestReadInstalledSaysWhyAndWhichRepositoryOrForeign(t *testing.T) {
|
|
m := machine(byLine(map[string]Ran{
|
|
"pacman -Q": {Stdout: "zsh 5.9-1\nglibc 2.42-1\nyay 12.0-1\n"},
|
|
"pacman -Qeq": {Stdout: "yay\nzsh\n"},
|
|
"pacman -Sl": {Stdout: "core glibc 2.42-1 [installed]\nextra zsh 5.9-1 [installed]\nextra-testing zsh 5.9-2\n"},
|
|
}, nil), 1000)
|
|
pk, skipped, err := m.ReadInstalled()
|
|
if err != nil || skipped != 0 {
|
|
t.Fatal(err, skipped)
|
|
}
|
|
want := []InstalledPackage{{"glibc", "2.42-1", false, "core"}, {"yay", "12.0-1", true, RepoForeign}, {"zsh", "5.9-1", true, "extra"}}
|
|
if !samePackages(pk, want) {
|
|
t.Fatalf("%+v", pk)
|
|
}
|
|
}
|
|
|
|
func TestOwnsAnswersNoOwnerAsAnAnswer(t *testing.T) {
|
|
m := machine(byLine(map[string]Ran{
|
|
"pacman -Qo -- /usr/bin/zsh": {Stdout: "/usr/bin/zsh is owned by zsh 5.9.2-1\n"},
|
|
"pacman -Qo -- /etc/hostname": {Status: 1, Stderr: "error: No package owns /etc/hostname\n"},
|
|
"pacman -Qo -- /nope": {Status: 1, Stderr: "error: failed to read file '/nope': No such file or directory\n"},
|
|
}, nil), 1000)
|
|
if r, err := m.Owns("/usr/bin/zsh"); err != nil || r["package"] != "zsh" || r["owned"] != true {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
if r, err := m.Owns("/etc/hostname"); err != nil || r["owned"] != false {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
if _, err := m.Owns("/nope"); err == nil {
|
|
t.Fatal("a path that is not there is an error")
|
|
}
|
|
if _, err := m.Owns("relative"); err == nil {
|
|
t.Fatal("a relative path was taken")
|
|
}
|
|
}
|
|
|
|
func TestUpdateListReadsCheckupdatesAndItsNothingToDo(t *testing.T) {
|
|
m := machine(byLine(map[string]Ran{"checkupdates": {Stdout: "zsh 5.9-1 -> 5.9-2\nlinux 6.1-1 -> 6.2-1\n"}}, nil), 1000)
|
|
r, err := m.UpdateList()
|
|
if err != nil || len(r) != 2 || r[0] != (VersionChange{"linux", "6.1-1", "6.2-1"}) {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
m = machine(byLine(map[string]Ran{"checkupdates": {Status: 2}}, nil), 1000)
|
|
if r, err := m.UpdateList(); err != nil || r == nil || len(r) != 0 {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
m = machine(byLine(map[string]Ran{"checkupdates": {Status: 1, Stderr: "==> ERROR: Cannot fetch updates\n"}}, nil), 1000)
|
|
if _, err := m.UpdateList(); err == nil || !strings.Contains(err.Error(), "Cannot fetch updates") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|
|
|
|
func lockless(m *Machine) *Machine {
|
|
m.ReadFile = func(p string) ([]byte, error) { return nil, errNoFile }
|
|
return m
|
|
}
|
|
|
|
func TestAnUpgradesUnitIsReadBack(t *testing.T) {
|
|
unit := "mesh-pacman-upgrade-1791115200"
|
|
m := machine(byLine(map[string]Ran{
|
|
"systemctl show " + unit + " --no-pager --property=LoadState --property=ActiveState --property=SubState --property=Result --property=ExecMainStatus": {Stdout: "LoadState=loaded\nActiveState=failed\nSubState=failed\nResult=exit-code\nExecMainStatus=1\n"},
|
|
"sudo -n journalctl --no-pager -o cat -n 60 -u " + unit: {Stdout: "error: failed to commit transaction (conflicting files)\n"},
|
|
}, nil), 1000)
|
|
r, err := m.UpgradeStatus(unit, 60)
|
|
if err != nil || r["finished"] != true || r["succeeded"] != false || r["exit_status"] != "1" || len(r["log"].([]string)) != 1 {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
if _, err := m.UpgradeStatus("sshd.service", 60); err == nil {
|
|
t.Fatal("a unit the tools did not start was read")
|
|
}
|
|
}
|
|
|
|
func orphanMachine(calls *[]call) *Machine {
|
|
return lockless(machine(fake(func(c call) Ran {
|
|
switch {
|
|
case c.String() == "pacman -Qdt":
|
|
return Ran{Stdout: "argon2 20190702-6\nclang21 21.1.8-1\n"}
|
|
case c.name == "sudo" && c.args[1] == "systemd-run":
|
|
return Ran{}
|
|
case c.name == "sudo" && c.args[1] == "journalctl":
|
|
return Ran{Stdout: "removing argon2...\n"}
|
|
}
|
|
return Ran{Status: 99}
|
|
}, calls), 1000))
|
|
}
|
|
|
|
func TestRemovingOrphansTakesOnlyOrphansNamedOrAll(t *testing.T) {
|
|
var calls []call
|
|
m := orphanMachine(&calls)
|
|
if _, err := m.RemoveOrphans(nil, false); err == nil || !strings.Contains(err.Error(), "name the orphans") {
|
|
t.Fatalf("nothing named: %v", err)
|
|
}
|
|
if _, err := m.RemoveOrphans([]string{"glibc"}, false); err == nil || !strings.Contains(err.Error(), "glibc is not an orphan") {
|
|
t.Fatalf("not an orphan: %v", err)
|
|
}
|
|
r, err := m.RemoveOrphans([]string{"argon2"}, false)
|
|
if err != nil || strings.Join(r["removed"].([]string), ",") != "argon2" {
|
|
t.Fatalf("%v %v", r, err)
|
|
}
|
|
var run string
|
|
for _, c := range calls {
|
|
if c.name == "sudo" && c.args[1] == "systemd-run" {
|
|
run = c.String()
|
|
}
|
|
}
|
|
if !strings.Contains(run, "--wait pacman -Rs --noconfirm -- argon2") {
|
|
t.Fatalf("ran: %s", run)
|
|
}
|
|
r, _ = m.RemoveOrphans(nil, true)
|
|
if len(r["removed"].([]string)) != 2 {
|
|
t.Fatalf("all: %v", r)
|
|
}
|
|
}
|
|
|
|
func TestCacheSaysWhatCleaningWouldFreeAndCleansThroughSudo(t *testing.T) {
|
|
var calls []call
|
|
m := machine(fake(func(c call) Ran {
|
|
switch c.String() {
|
|
case "find /var/cache/pacman/pkg -mindepth 1 -maxdepth 1 -printf %y %s %f\n":
|
|
return Ran{Status: 1, Stdout: "f 1000 zsh-5.9-1-x86_64.pkg.tar.zst\nf 10 zsh-5.9-1-x86_64.pkg.tar.zst.sig\nd 4096 download-abc\n", Stderr: "find: permission denied\n"}
|
|
case "paccache -d -k 3":
|
|
return Ran{Stdout: "\n==> finished dry run: 12 candidates (disk space saved: 1.5 GiB)\n"}
|
|
case "sudo -n paccache -r -k 3":
|
|
return Ran{Stdout: "==> finished: 12 packages removed (disk space saved: 1.5 GiB)\n"}
|
|
}
|
|
if c.name == "systemctl" {
|
|
return Ran{Stdout: "ActiveState=active\nUnitFileState=enabled\n"}
|
|
}
|
|
return Ran{Status: 99}
|
|
}, &calls), 1000)
|
|
c, err := m.Cache(3, false, false)
|
|
if err != nil || c.Files != 1 || c.Bytes != 1010 || c.LeftDownloads != 1 || c.Candidates != 12 || c.Frees != "1.5 GiB" || c.Cleaned {
|
|
t.Fatalf("%+v %v", c, err)
|
|
}
|
|
c, err = m.Cache(3, false, true)
|
|
if err != nil || !c.Cleaned || c.Candidates != 12 || c.Timer["UnitFileState"] != "enabled" {
|
|
t.Fatalf("%+v %v", c, err)
|
|
}
|
|
}
|
|
|
|
const pacmanLog = `[2026-09-24T17:47:36+0200] [PACMAN] starting full system upgrade
|
|
[2026-09-24T17:48:00+0200] [ALPM] upgraded linux (6.1-1 -> 6.2-1)
|
|
[2026-09-24T17:48:01+0200] [ALPM] installed zsh (5.9-1)
|
|
[2026-10-02T09:00:00+0200] [ALPM] removed ntp (4.2.8-1)
|
|
[2026-10-02T09:00:00+0200] [ALPM-SCRIPTLET] some words
|
|
[2022-01-01 10:00] [ALPM] installed old (1-1)
|
|
`
|
|
|
|
func TestHistoryReadsTheLogSinceADayAndTheLastFullUpgrade(t *testing.T) {
|
|
m := machine(nil, 1000)
|
|
m.ReadFile = func(p string) ([]byte, error) { return []byte(pacmanLog), nil }
|
|
r, err := m.History("2026-09-01", "", "", 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ev := r["events"].([]Event)
|
|
if len(ev) != 3 || ev[0].From != "6.1-1" || ev[0].Version != "6.2-1" || ev[2].Action != "removed" {
|
|
t.Fatalf("%+v", ev)
|
|
}
|
|
if r["last_full_upgrade"] != "2026-09-24T17:47:36+02:00" {
|
|
t.Fatalf("%v", r["last_full_upgrade"])
|
|
}
|
|
r, _ = m.History("2026-09-01", "removed", "", 10)
|
|
if r["count"] != 1 {
|
|
t.Fatalf("%v", r)
|
|
}
|
|
r, _ = m.History("2026-09-01", "", "", 1)
|
|
if r["truncated"] != true || r["events"].([]Event)[0].Package != "ntp" {
|
|
t.Fatalf("the newest are kept: %v", r)
|
|
}
|
|
if _, err := m.History("yesterday", "", "", 1); err == nil {
|
|
t.Fatal("not a day")
|
|
}
|
|
if _, err := m.History("", "exploded", "", 1); err == nil {
|
|
t.Fatal("not an action")
|
|
}
|
|
}
|
|
|
|
const feed = `<?xml version="1.0" encoding="utf-8"?>
|
|
<rss version="2.0"><channel><title>Arch Linux: Recent news updates</title>
|
|
<item><title>Manual intervention needed</title><link>https://example.org/news/a/</link><description><p>Do this <b>first</b>.</p></description><pubDate>Tue, 22 Sep 2026 09:09:27 +0000</pubDate></item>
|
|
<item><title>Old news</title><link>https://example.org/news/b/</link><description>old</description><pubDate>Mon, 01 Jun 2026 09:00:00 +0000</pubDate></item>
|
|
</channel></rss>`
|
|
|
|
func TestNewsSinceTheLastUpgrade(t *testing.T) {
|
|
items, err := ParseNews([]byte(feed), time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC))
|
|
if err != nil || len(items) != 1 || items[0].Title != "Manual intervention needed" || items[0].Summary != "Do this first ." {
|
|
t.Fatalf("%+v %v", items, err)
|
|
}
|
|
fetch = func(string) ([]byte, error) { return []byte(feed), nil }
|
|
m := machine(nil, 1000)
|
|
m.ReadFile = func(p string) ([]byte, error) { return []byte(pacmanLog), nil }
|
|
n := m.News("")
|
|
if n["reachable"] != true || n["since_last_full_upgrade"] != true || len(n["items"].([]NewsItem)) != 0 {
|
|
t.Fatalf("after the last upgrade on the 24th, the post of the 22nd is old: %v", n)
|
|
}
|
|
}
|
|
|
|
const mirrorlistReflector = `################################################################################
|
|
################# Arch Linux mirrorlist generated by Reflector #################
|
|
################################################################################
|
|
|
|
# With: reflector @/etc/xdg/reflector/reflector.conf
|
|
# When: 2024-06-12 21:26:34 UTC
|
|
|
|
Server = https://mirror.example.org/archlinux/$repo/os/$arch
|
|
Server = https://mirror2.example.org/$repo/os/$arch
|
|
#Server = https://old.example.org/$repo/os/$arch
|
|
`
|
|
|
|
// reflectorInfo is reflector's --info as it answered on 2026-11-01 (reflector 2023, its fields cut to those
|
|
// read and one more), the ranked order kept: two mirrors at the newest last sync, one behind, one with none.
|
|
const reflectorInfo = `https://nl.mirror.example/archlinux/$repo/os/$arch
|
|
active : True
|
|
country : Netherlands
|
|
last_sync : 2026-11-01 14:32:01 UTC
|
|
|
|
https://be.mirror.example/archlinux/$repo/os/$arch
|
|
active : True
|
|
last_sync : 2026-11-01 14:01:45 UTC
|
|
|
|
https://de.mirror.example/$repo/os/$arch
|
|
last_sync : 2026-11-01 14:32:01 UTC
|
|
|
|
https://fr.mirror.example/archlinux/$repo/os/$arch
|
|
last_sync : None
|
|
`
|
|
|
|
// rankedAt is after every last sync in reflectorInfo: reflector ran then.
|
|
var rankedAt = time.Date(2026, 11, 1, 15, 0, 0, 0, time.UTC)
|
|
|
|
// ADR 0586 §1, ADR 0585 §4: the ranking keeps the mirrors at the newest sync and a tail of the next freshest, in reflector's order, with the module's options
|
|
// and never its --save; a ranking that fails or keeps none is no list. pacman_mirrors answers the shared list, its
|
|
// ranker and age and whether this node's file matches it (the file as found when not); with refresh it asks this node's
|
|
// keeper to rank again, refused while a round runs, naming it.
|
|
func TestTheMeshsMirrorListIsRankedFreshAndAnsweredWithThisNodesFile(t *testing.T) {
|
|
conf := "# comment\n--save /etc/pacman.d/mirrorlist\n--country Belgium,Netherlands\n--sort rate\n"
|
|
if o := strings.Join(ReflectorOptions(conf), " "); o != "--country Belgium,Netherlands --sort rate" {
|
|
t.Errorf("the options are %q", o)
|
|
}
|
|
var asked []string
|
|
answer := Ran{Stdout: reflectorInfo}
|
|
saved := reflectorRun
|
|
reflectorRun = func(_ context.Context, args ...string) Ran { asked = args; return answer }
|
|
defer func() { reflectorRun = saved }()
|
|
l, err := RankMirrors([]byte(conf), "node-a", rankedAt, "2026-W43")
|
|
if err != nil || strings.Join(l.Servers, " ") != "https://nl.mirror.example/archlinux/$repo/os/$arch https://de.mirror.example/$repo/os/$arch https://be.mirror.example/archlinux/$repo/os/$arch" || l.Freshest != 2 ||
|
|
!l.Newest.Equal(time.Date(2026, 11, 1, 14, 32, 1, 0, time.UTC)) || l.RankedBy != "node-a" || l.Round != "2026-W43" ||
|
|
strings.Join(asked, " ") != "--country Belgium,Netherlands --sort rate --info" {
|
|
t.Fatalf("ranked %+v (%v), reflector asked %v", l, err, asked)
|
|
}
|
|
for _, a := range []Ran{{Status: 1, Stderr: "error: no mirrors"}, {Stdout: "https://x.example/$repo/os/$arch\nlast_sync : None\n"}} {
|
|
answer = a
|
|
if _, err := RankMirrors([]byte(conf), "node-a", rankedAt, ""); err == nil {
|
|
t.Errorf("a ranking from %+v is a list", a)
|
|
}
|
|
}
|
|
checkFreshest(t)
|
|
if checkServers([]string{"https://ok.example/$repo/os/$arch\nServer = https://evil.example/$repo/os/$arch"}) == nil {
|
|
t.Error("a server carrying a second line is taken")
|
|
}
|
|
|
|
states := &fakeStates{}
|
|
states.store(StateMirrors).Put(MirrorsKey, l)
|
|
file := RenderMirrorlist(l.Servers)
|
|
m := machine(func(context.Context, string, ...string) Ran {
|
|
return Ran{Stdout: "LoadState=loaded\nUnitFileState=disabled\nActiveState=inactive\n"}
|
|
}, 1000)
|
|
m.Node, m.States = "node-a", readerOf(states)
|
|
m.Now = func() time.Time { return rankedAt.Add(90 * time.Minute) }
|
|
m.ReadFile = func(string) ([]byte, error) { return file, nil }
|
|
v, err := m.MirrorsView(false)
|
|
if err != nil || v["file_matches"] != true || v["ranked_by"] != "node-a" || v["age"] != "1h30m0s" || v["file"] != nil ||
|
|
v["reflector_timer"].(map[string]string)["UnitFileState"] != "disabled" {
|
|
t.Fatalf("the view is %v (%v)", v, err)
|
|
}
|
|
file = []byte(mirrorlistReflector)
|
|
if v, _ := m.MirrorsView(false); v["file_matches"] != false || v["file"].(Mirrors).GeneratedBy != "reflector" {
|
|
t.Errorf("a file other than the list is %v", v)
|
|
}
|
|
|
|
rounds, runs, asks := newBucket(), newBucket(), newBucket()
|
|
savedState, savedNode := upgradeState, thisNode
|
|
upgradeState = func(name string) Bucket {
|
|
return map[string]Bucket{StateRounds: rounds, StateRuns: runs, StateAsks: asks}[name]
|
|
}
|
|
thisNode = func() string { return "node-a" }
|
|
defer func() { upgradeState, thisNode = savedState, savedNode }()
|
|
rounds.Put("2026-W43", Round{Round: "2026-W43", Opened: t0, OpenedBy: "node-a", Order: []string{"node-a", "node-b"}})
|
|
if _, err := m.MirrorsView(true); err == nil || !strings.Contains(err.Error(), "round 2026-W43 runs") {
|
|
t.Errorf("a refresh during a round: %v", err)
|
|
}
|
|
if keys, _ := asks.Keys(); len(keys) != 0 {
|
|
t.Errorf("a refused refresh asked %v", keys)
|
|
}
|
|
rounds.Delete("2026-W43")
|
|
v, err = m.MirrorsView(true)
|
|
if keys, _ := asks.Keys(); err != nil || len(keys) != 1 || !strings.HasPrefix(keys[0], "node-a.") || v["asked"] == nil {
|
|
t.Errorf("a refresh asked %v (%v): %v", keys, err, v)
|
|
}
|
|
}
|
|
|
|
func TestConfigIsReadAsPacmanConfResolvesIt(t *testing.T) {
|
|
c := ParseConf("[options]\nHoldPkg = pacman\nHoldPkg = glibc\nCheckSpace\nParallelDownloads = 5\n[core]\nUsage = All\nServer = https://a/core\nServer = https://b/core\n[extra]\nServer = https://a/extra\n")
|
|
if len(c.Options["HoldPkg"]) != 2 || c.Options["ParallelDownloads"][0] != "5" || len(c.Repositories) != 2 || c.Repositories[0].Servers != 2 || c.Repositories[0].FirstServer != "https://a/core" {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
}
|
|
|
|
// ADR 0586 §1: the freshest first, then up to four of the next freshest, newest first; an unknown last sync is left out,
|
|
// whether reflector wrote None or the moment it ran (gmtime(None)).
|
|
func checkFreshest(t *testing.T) {
|
|
t.Helper()
|
|
at := func(m int) time.Time { return time.Date(2026, 11, 1, 14, m, 0, 0, time.UTC) }
|
|
mk := func(syncs ...int) []rankedMirror {
|
|
var r []rankedMirror
|
|
for i, m := range syncs {
|
|
mm := rankedMirror{URL: fmt.Sprintf("m%d", i)}
|
|
if m >= 0 {
|
|
mm.LastSync = at(m)
|
|
}
|
|
r = append(r, mm)
|
|
}
|
|
return r
|
|
}
|
|
for _, c := range []struct {
|
|
name string
|
|
ranked []rankedMirror
|
|
servers string
|
|
fresh int
|
|
}{
|
|
{"all fresh", mk(30, 30, 30), "m0 m1 m2", 3},
|
|
{"one fresh and a tail of four", mk(10, 30, 20, 25, 5, 1), "m1 m3 m2 m0 m4", 1},
|
|
{"more than four candidates", mk(30, 29, 28, 27, 26, 25, 24), "m0 m1 m2 m3 m4", 1},
|
|
{"two fresh and the tail", mk(30, 20, 30, 10, 11, 12, 13), "m0 m2 m1 m6 m5 m4", 2},
|
|
{"unknown last sync left out", mk(30, -1, 20, -1), "m0 m2", 1},
|
|
{"none known", mk(-1, -1), "", 0},
|
|
} {
|
|
got, _, fresh := Freshest(c.ranked)
|
|
if strings.Join(got, " ") != c.servers || fresh != c.fresh {
|
|
t.Errorf("%s: kept %v, %d freshest; want %q, %d", c.name, got, fresh, c.servers, c.fresh)
|
|
}
|
|
}
|
|
// reflector prints an unknown last sync as the moment it ran: at or after the start of the run is no real sync.
|
|
start := time.Date(2026, 11, 1, 15, 0, 0, 500, time.UTC)
|
|
r := []rankedMirror{{"a", at(30)}, {"b", start.Truncate(time.Second)}, {"c", start.Add(time.Second)}}
|
|
withoutUnknownSync(r, start)
|
|
if r[0].LastSync.IsZero() || !r[1].LastSync.IsZero() || !r[2].LastSync.IsZero() {
|
|
t.Errorf("unknown syncs not detected: %+v", r)
|
|
}
|
|
}
|