Files
mesh-catalog/modules/power/cmd/power/watcher_test.go
T
jochen 6315556152 power: a machine's power as a module holding node-power; the laptop's resume and lid move onto it (hq ADR 0211)
Code around sleep was written into the service manager's sleep units by the module that needed it,
and the mesh could not tell a sleeping machine from a lost one. power runs every module's code for
the six moments, each piece bounded, owns logind's power handling from its settings, and says
booted, sleeping, woke, shutting-down and the power source on the bus, sleeping under logind's
delay lock before the machine sleeps.
2026-10-04 17:19:27 +02:00

196 lines
5.1 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 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())
}
}