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 }