Files
mesh-host/internal/profile/power_test.go
T
jochen e41ab3a505 Report whether a battery powers the machine and whether anything measures its draw (hq ADR 0255)
The power module is on every machine and its bar blocks apply only where the
hardware is. Two capabilities read from the kernel let the controller compose them
there and nowhere else: battery (a Battery supply not scoped to a device) and
power-meter (a system battery, a powercap package counter, or a graphics device
reporting its power).
2026-10-08 14:44:55 +02:00

100 lines
3.6 KiB
Go

package profile
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
// Defends novox/hq ADR 0255: a machine says whether a battery powers it and whether anything on it
// measures the power it draws, read from the kernel, so the controller composes a battery's or a power
// draw's block only where there is one.
func write(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}
// sysfs points every power source at a directory of its own, empty unless a case fills it.
func sysfs(t *testing.T) string {
t.Helper()
dir := t.TempDir()
old := [4]string{PowerSupplySource, PowercapSource, SeatSource, NvidiaSource}
PowerSupplySource = filepath.Join(dir, "power_supply")
PowercapSource = filepath.Join(dir, "powercap")
SeatSource = filepath.Join(dir, "drm")
NvidiaSource = filepath.Join(dir, "nvidia")
t.Cleanup(func() { PowerSupplySource, PowercapSource, SeatSource, NvidiaSource = old[0], old[1], old[2], old[3] })
return dir
}
func TestALaptopHasABatteryAndAMeter(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/power_supply/ACAD/type", "Mains")
write(t, dir+"/power_supply/BAT1/type", "Battery")
write(t, dir+"/power_supply/BAT1/scope", "System")
write(t, dir+"/power_supply/BAT1/present", "1")
write(t, dir+"/powercap/intel-rapl:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1")
b := battery{}.Detect(context.Background())
if !b.Present || b.Detail != "BAT1" {
t.Fatalf("battery: %+v", b)
}
m := powerMeter{}.Detect(context.Background())
if !m.Present || m.Detail != "battery BAT1, processor package intel-rapl:0" {
t.Fatalf("power meter: %+v", m)
}
}
func TestADevicesBatteryIsNotTheMachines(t *testing.T) {
// The case worth a test: a desktop with a wireless mouse reports a Battery, and a battery block
// composed for it would show the mouse's charge as the machine's.
dir := sysfs(t)
write(t, dir+"/power_supply/hidpp_battery_0/type", "Battery")
write(t, dir+"/power_supply/hidpp_battery_0/scope", "Device")
write(t, dir+"/power_supply/BAT0/type", "Battery")
write(t, dir+"/power_supply/BAT0/present", "0")
if b := (battery{}).Detect(context.Background()); b.Present {
t.Fatalf("a mouse's battery, or an empty bay, was taken for the machine's: %+v", b)
}
if m := (powerMeter{}).Detect(context.Background()); m.Present {
t.Fatalf("a device's battery was taken for a meter: %+v", m)
}
}
func TestADesktopMeasuresItsProcessorAndItsGraphics(t *testing.T) {
dir := sysfs(t)
write(t, dir+"/powercap/intel-rapl:0/name", "package-0")
write(t, dir+"/powercap/intel-rapl:0/energy_uj", "1")
// A sub-domain is part of the package, not a second meter.
write(t, dir+"/powercap/intel-rapl:0:0/name", "core")
write(t, dir+"/powercap/intel-rapl:0:0/energy_uj", "1")
write(t, dir+"/drm/card1/device/hwmon/hwmon4/power1_average", "8000000")
write(t, dir+"/nvidia/0000:01:00.0/information", "x")
if b := (battery{}).Detect(context.Background()); b.Present {
t.Fatalf("a desktop has a battery: %+v", b)
}
m := powerMeter{}.Detect(context.Background())
if !m.Present || m.Detail != "processor package intel-rapl:0, graphics card1, graphics nvidia" {
t.Fatalf("power meter: %+v", m)
}
}
func TestAMachineWithNothingToReadHasNeither(t *testing.T) {
sysfs(t)
b, m := battery{}.Detect(context.Background()), powerMeter{}.Detect(context.Background())
if b.Present || m.Present || b.How == "" || m.How == "" || !strings.Contains(m.Detail, "nothing") {
t.Fatalf("%+v %+v", b, m)
}
}