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()) } }