Files
mesh-catalog/modules/power/cmd/power/watcher_test.go
T

226 lines
6.0 KiB
Go

package main
import (
"context"
"errors"
"os"
"path/filepath"
"reflect"
"sync"
"testing"
"time"
)
type fakeLogin struct {
mu sync.Mutex
held int
signals chan Signal
}
func (f *fakeLogin) Inhibit(what, who, why string) (*os.File, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
r.Close()
f.mu.Lock()
f.held++
f.mu.Unlock()
return w, nil
}
func (f *fakeLogin) Signals() <-chan Signal { return f.signals }
func (f *fakeLogin) Close() {}
type bus struct {
mu sync.Mutex
down bool
got []string
}
func (b *bus) emit(t string, _ any) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.down {
return errors.New("no bus")
}
b.got = append(b.got, t)
return nil
}
func (b *bus) seen() []string {
b.mu.Lock()
defer b.mu.Unlock()
return append([]string(nil), b.got...)
}
func init() { stillHeld = func(*os.File) bool { return true } }
func TestALostLockIsTakenAgain(t *testing.T) {
l := &fakeLogin{}
w := watcher(t, machine(t, nil), &bus{})
w.inhibit(l)
held := false
stillHeld = func(*os.File) bool { return held }
defer func() { stillHeld = func(*os.File) bool { return true } }()
w.verify(l)
if l.held != 2 || !w.Snapshot().Inhibiting {
t.Fatalf("a lock that was no longer held was not taken again: held %d %+v", l.held, w.Snapshot())
}
}
func TestAProblemIsNoLongerSaidOnceTheLockIsHeld(t *testing.T) {
w := watcher(t, machine(t, nil), &bus{})
w.problem("no delay lock: Permission denied")
w.inhibit(&fakeLogin{})
if s := w.Snapshot(); !s.Inhibiting || s.Problem != "" {
t.Fatalf("%+v", s)
}
}
func TestStdinIsNeverTakenForALock(t *testing.T) {
if IsInhibitor(0) || IsInhibitor(1) || IsInhibitor(2) {
t.Fatal("a standard descriptor was taken for logind's lock")
}
}
func machine(t *testing.T, files map[string]string) *Machine {
t.Helper()
root := t.TempDir()
for p, c := range files {
full := filepath.Join(root, p)
os.MkdirAll(filepath.Dir(full), 0o755)
os.WriteFile(full, []byte(c), 0o644)
}
return &Machine{Root: root}
}
func watcher(t *testing.T, m *Machine, b *bus) *Watcher {
w := NewWatcher(m, b.emit, nil)
w.state = filepath.Join(t.TempDir(), "announced-boot")
return w
}
func TestSleepingIsSaidBeforeTheLockGoesAndWokeAfter(t *testing.T) {
b := &bus{}
l := &fakeLogin{}
w := watcher(t, machine(t, nil), b)
w.inhibit(l)
w.handle(l, Signal{Start: true})
if got := b.seen(); !reflect.DeepEqual(got, []string{Sleeping}) {
t.Fatalf("before sleep the bus has %v", got)
}
if w.Snapshot().Inhibiting {
t.Fatal("the delay lock is still held after the announcement: the machine would wait to sleep")
}
w.handle(l, Signal{Start: false})
if got := b.seen(); !reflect.DeepEqual(got, []string{Sleeping, Woke}) {
t.Fatalf("after waking the bus has %v", got)
}
if !w.Snapshot().Inhibiting || l.held != 2 {
t.Fatalf("the lock is not taken again after waking: %+v, held %d", w.Snapshot(), l.held)
}
}
func TestABusThatIsGoneDoesNotHoldTheSleepAndTheEventsFollowInOrder(t *testing.T) {
b := &bus{down: true}
l := &fakeLogin{}
w := watcher(t, machine(t, nil), b)
w.inhibit(l)
start := time.Now()
w.handle(l, Signal{Start: true})
if time.Since(start) > AnnounceWithin+time.Second {
t.Fatal("the sleep waited longer than its bound for a bus that is gone")
}
if w.Snapshot().Inhibiting {
t.Fatal("the lock was kept while the bus was gone")
}
w.handle(l, Signal{Start: false})
if s := w.Snapshot(); s.Pending != 2 || s.Problem == "" {
t.Fatalf("what the bus did not take is not kept and said: %+v", s)
}
b.mu.Lock()
b.down = false
b.mu.Unlock()
w.flush()
if got := b.seen(); !reflect.DeepEqual(got, []string{Sleeping, Woke}) {
t.Fatalf("the late events arrived as %v", got)
}
if s := w.Snapshot(); s.Pending != 0 || s.Problem != "" {
t.Fatalf("after the bus came back: %+v", s)
}
}
func TestShutdownIsSaidAndLetGo(t *testing.T) {
b := &bus{}
l := &fakeLogin{}
w := watcher(t, machine(t, nil), b)
w.inhibit(l)
w.handle(l, Signal{Shutdown: true, Start: true})
if got := b.seen(); !reflect.DeepEqual(got, []string{ShuttingDown}) || w.Snapshot().Inhibiting {
t.Fatalf("%v, inhibiting %v", got, w.Snapshot().Inhibiting)
}
}
func TestBootIsSaidOncePerBootNotPerRestart(t *testing.T) {
m := machine(t, map[string]string{"/proc/sys/kernel/random/boot_id": "abc\n", "/proc/stat": "btime 1790000000\n"})
b := &bus{}
w := watcher(t, m, b)
w.bootOnce()
w.flush()
again := NewWatcher(m, b.emit, nil)
again.state = w.state
again.bootOnce()
again.flush()
if got := b.seen(); !reflect.DeepEqual(got, []string{Booted}) {
t.Fatalf("a restart of the runtime was taken for a boot: %v", got)
}
}
func supplies(online string, capacity string) map[string]string {
return map[string]string{
"/sys/class/power_supply/ACAD/type": "Mains", "/sys/class/power_supply/ACAD/online": online,
"/sys/class/power_supply/BAT1/type": "Battery", "/sys/class/power_supply/BAT1/capacity": capacity,
"/sys/class/power_supply/BAT1/status": "Discharging",
}
}
func TestASupplyChangeIsSaidAndALowBatteryOncePerDischarge(t *testing.T) {
b := &bus{}
m := machine(t, supplies("1", "50"))
w := watcher(t, m, b)
w.supply() // baseline: says nothing
m2 := machine(t, supplies("0", "9"))
w.m = m2
w.supply()
w.supply()
w.flush()
if got := b.seen(); !reflect.DeepEqual(got, []string{OnBattery, BatteryLow}) {
t.Fatalf("%v", got)
}
}
func TestAMachineWithoutMainsSaysNothingAboutSupplies(t *testing.T) {
b := &bus{}
w := watcher(t, machine(t, nil), b)
w.supply()
w.supply()
w.flush()
if got := b.seen(); len(got) != 0 {
t.Fatalf("a server announced %v", got)
}
}
func TestRunWithoutLogindStillSaysTheBoot(t *testing.T) {
m := machine(t, map[string]string{"/proc/sys/kernel/random/boot_id": "xyz\n"})
b := &bus{}
w := NewWatcher(m, b.emit, func() (Login1, error) { return nil, errors.New("no system bus") })
w.state = filepath.Join(t.TempDir(), "b")
w.poll = 10 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
w.Run(ctx)
if got := b.seen(); len(got) == 0 || got[0] != Booted || w.Snapshot().Problem == "" {
t.Fatalf("%v %+v", got, w.Snapshot())
}
}