A failed read keeps the last list and says reachable false, so a list the node can no longer confirm is never shown as current, and a first read that fails is never an empty list taken for an answer.
148 lines
4.5 KiB
Go
148 lines
4.5 KiB
Go
package main
|
|
|
|
// What an upgrade would change (novox/hq ADR 0507 §4; acceptance 1840/AC4): read at the keeper's start, after a
|
|
// transaction, after the mirror list is rewritten and a day after the last read; written only when the list or
|
|
// reachable changed; a failed read says reachable false and keeps the last list.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func updatesOf(t *testing.T, s *fakeStore) UpdatesValue {
|
|
t.Helper()
|
|
e, _ := s.Get("node-a")
|
|
if e == nil {
|
|
t.Fatal("no updates.node-a")
|
|
}
|
|
var v UpdatesValue
|
|
if err := json.Unmarshal(e.Value, &v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (d *pacmanDB) checkupdatesRuns() int {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
n := 0
|
|
for _, c := range d.commands {
|
|
if c == "checkupdates" {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestUpdatesAreWrittenOnlyWhenTheListOrReachableChanged(t *testing.T) {
|
|
db := &pacmanDB{sl: syncDBs, checkup: Ran{Stdout: "zsh 5.9-1 -> 5.9-2\nlinux 6.17.1-1 -> 6.17.2-1\n"}}
|
|
db.set("glibc 2.42-1\n", "")
|
|
k, states, _, _, _ := newTestKeeper(db)
|
|
up := states.store(StateUpdates)
|
|
k.refreshUpdates()
|
|
if w := up.Writes(); strings.Join(w, ",") != "put node-a" {
|
|
t.Fatalf("writes: %v", w)
|
|
}
|
|
if v := updatesOf(t, up); !v.Reachable || fmt.Sprint(v.Packages) != "[{linux 6.17.1-1 6.17.2-1} {zsh 5.9-1 5.9-2}]" {
|
|
t.Fatalf("%+v", v)
|
|
}
|
|
// the same answer: nothing written.
|
|
k.refreshUpdates()
|
|
if w := up.Writes(); len(w) != 0 {
|
|
t.Fatalf("the same list was written again: %v", w)
|
|
}
|
|
// the mirrors cannot be reached: reachable false, the last list kept.
|
|
db.setUpdates(Ran{Status: 1, Stderr: "==> ERROR: Cannot fetch updates\n"})
|
|
k.refreshUpdates()
|
|
if w := up.Writes(); len(w) != 1 {
|
|
t.Fatalf("writes: %v", w)
|
|
}
|
|
if v := updatesOf(t, up); v.Reachable || len(v.Packages) != 2 {
|
|
t.Fatalf("a failed read keeps the list and says reachable false: %+v", v)
|
|
}
|
|
k.refreshUpdates()
|
|
if w := up.Writes(); len(w) != 0 {
|
|
t.Fatalf("still unreachable, and written again: %v", w)
|
|
}
|
|
// reached again with nothing to do (status 2): an empty list, reachable.
|
|
db.setUpdates(Ran{Status: 2})
|
|
k.refreshUpdates()
|
|
if v := updatesOf(t, up); !v.Reachable || v.Packages == nil || len(v.Packages) != 0 {
|
|
t.Fatalf("%+v", v)
|
|
}
|
|
}
|
|
|
|
func TestAFirstReadThatFailsSaysUnreachableNeverAnEmptyListAsAnAnswer(t *testing.T) {
|
|
db := &pacmanDB{sl: syncDBs, checkup: Ran{Status: 1, Stderr: "==> ERROR: Cannot fetch updates\n"}}
|
|
k, states, _, _, _ := newTestKeeper(db)
|
|
k.refreshUpdates()
|
|
if v := updatesOf(t, states.store(StateUpdates)); v.Reachable {
|
|
t.Fatalf("%+v", v)
|
|
}
|
|
raw, _ := states.store(StateUpdates).Get("node-a")
|
|
if string(raw.Value) != `{"reachable":false,"packages":[]}` {
|
|
t.Errorf("%s", raw.Value)
|
|
}
|
|
}
|
|
|
|
// Read at the start, after a transaction, after the mirror list is rewritten, and a day after the last read
|
|
// with nothing else asking; never in between.
|
|
func TestUpdatesAreReadAtStartAfterATransactionAfterTheMirrorsAndDaily(t *testing.T) {
|
|
db := &pacmanDB{sl: syncDBs, checkup: Ran{Status: 2}}
|
|
db.set("glibc 2.42-1\n", "")
|
|
k, states, _, clock, w := newTestKeeper(db)
|
|
var mu sync.Mutex
|
|
var seen []string
|
|
k.Hooks.AfterTransaction = []func(*Keeper, Transaction) error{func(*Keeper, Transaction) error {
|
|
mu.Lock()
|
|
seen = append(seen, "transaction")
|
|
mu.Unlock()
|
|
return nil
|
|
}}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
done := make(chan error, 1)
|
|
go func() { done <- k.Run(ctx) }()
|
|
reads := func(want int) {
|
|
t.Helper()
|
|
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
|
|
if db.checkupdatesRuns() == want {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("checkupdates ran %d times, want %d", db.checkupdatesRuns(), want)
|
|
}
|
|
reads(1)
|
|
db.setUpdates(Ran{Stdout: "zsh 5.9-1 -> 5.9-2\n"})
|
|
clock.Advance(time.Hour)
|
|
w.c <- MirrorsRewritten
|
|
reads(2)
|
|
db.setUpdates(Ran{Status: 2})
|
|
w.c <- LockReleased
|
|
reads(3)
|
|
waitFor(t, &mu, &seen, "transaction")
|
|
clock.Advance(UpdatesEvery - time.Minute)
|
|
time.Sleep(50 * time.Millisecond)
|
|
if n := db.checkupdatesRuns(); n != 3 {
|
|
t.Fatalf("read before a day passed: %d", n)
|
|
}
|
|
db.setUpdates(Ran{Stdout: "glibc 2.42-1 -> 2.42-2\n"})
|
|
clock.Advance(time.Minute)
|
|
reads(4)
|
|
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(time.Millisecond) {
|
|
if e, _ := states.store(StateUpdates).Get("node-a"); e != nil && strings.Contains(string(e.Value), "glibc") {
|
|
break
|
|
}
|
|
}
|
|
if w := states.store(StateUpdates).Writes(); len(w) != 4 {
|
|
t.Errorf("each read that changed the list wrote it, and only those: %v", w)
|
|
}
|
|
cancel()
|
|
<-done
|
|
}
|