226 lines
6.0 KiB
Go
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())
|
|
}
|
|
}
|