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.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user