Files
mesh-catalog/modules/power/cmd/power/draw_test.go
T
jochen f2ab795926 Let the bar render blocks other modules contribute, and give it the battery and power draw from the power module (hq ADR 0255)
The bar left the battery out because it belongs to whoever knows the hardware, and had
no way to take it back in. i3status-rust now renders bar-neutral blocks through its
template; the power module contributes the battery where the machine reports one and
the power draw where something measures it, sampling the root-only energy counter as
root and publishing only a rate. The memory block shows what programs hold, the figure
memory-pressure judges by, instead of counting the cache as used.
2026-10-08 14:33:43 +02:00

138 lines
4.9 KiB
Go

package main
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// Defends novox/hq ADR 0255: the power drawn is measured honestly and says what it measured — the
// machine on battery, the processor (and discrete graphics) otherwise — and only a rate leaves root.
func tree(t *testing.T, files map[string]string) *Machine {
t.Helper()
root := t.TempDir()
for p, c := range files {
full := filepath.Join(root, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte(c+"\n"), 0o644); err != nil {
t.Fatal(err)
}
}
return &Machine{Root: root, Run: func(context.Context, string, ...string) (string, error) {
return "", errors.New("nothing may be run here")
}}
}
func noWait(time.Duration) bool { return true }
func TestOnBatteryTheDischargeIsTheMachine(t *testing.T) {
m := tree(t, map[string]string{
"/sys/class/power_supply/BAT1/type": "Battery",
"/sys/class/power_supply/BAT1/status": "Discharging",
"/sys/class/power_supply/BAT1/current_now": "-1500000",
"/sys/class/power_supply/BAT1/voltage_now": "16000000",
// A counter is there, and not what a battery reading reports.
"/sys/class/powercap/intel-rapl:0/name": "package-0",
"/sys/class/powercap/intel-rapl:0/energy_uj": "1",
})
d := m.Sample(context.Background(), time.Second, func(time.Duration) bool { t.Fatal("a battery reading waited"); return false })
if got := d.Line(); got != "24.0 W machine" {
t.Fatalf("%q", got)
}
}
func TestOnMainsTheProcessorIsMeasuredAndCalledTheProcessor(t *testing.T) {
m := tree(t, map[string]string{
"/sys/class/power_supply/BAT1/type": "Battery",
"/sys/class/power_supply/BAT1/status": "Not charging",
"/sys/class/powercap/intel-rapl:0/name": "package-0",
"/sys/class/powercap/intel-rapl:0/energy_uj": "1000000",
"/sys/class/powercap/intel-rapl:0/max_energy_range_uj": "65532610987",
"/sys/class/powercap/intel-rapl:0:0/name": "core",
"/sys/class/powercap/intel-rapl:0:0/energy_uj": "1",
// An APU's integrated graphics report the whole package: not counted a second time.
"/sys/class/drm/card1/device/hwmon/hwmon5/power1_average": "19000000",
})
counter := filepath.Join(m.Root, "/sys/class/powercap/intel-rapl:0/energy_uj")
d := m.Sample(context.Background(), time.Second, func(time.Duration) bool {
return os.WriteFile(counter, []byte("11000000\n"), 0o644) == nil
})
if d.Machine != nil || d.CPU == nil || d.GPU != nil || *d.CPU <= 0 {
t.Fatalf("%+v", d)
}
if !strings.HasSuffix(d.Line(), " W CPU") {
t.Fatalf("%q", d.Line())
}
}
func TestACounterThatWrappedIsCountedAcrossItsRange(t *testing.T) {
before := map[string]counter{"p": {uj: 90, max: 100}}
after := map[string]counter{"p": {uj: 10, max: 100}}
w := cpuWatts(before, after, time.Second)
if w == nil || *w != 20e-6 {
t.Fatalf("%v", w)
}
if cpuWatts(before, map[string]counter{"other": {uj: 1}}, time.Second) != nil {
t.Fatal("a counter read once was taken for a rate")
}
}
func TestADiscreteGraphicsDevicesPowerIsAddedAndASleepingOneIsNotWoken(t *testing.T) {
m := tree(t, map[string]string{
"/sys/class/drm/card0/device/mem_info_vram_vendor": "samsung",
"/sys/class/drm/card0/device/hwmon/hwmon3/power1_average": "30000000",
"/sys/class/drm/card0-DP-1/status": "connected",
"/proc/driver/nvidia/gpus/0000:01:00.0/information": "x",
"/sys/bus/pci/devices/0000:01:00.0/power/runtime_status": "suspended",
})
if w := m.gpuWatts(context.Background()); w == nil || *w != 30 {
t.Fatalf("%v", w)
}
// Awake, it is asked.
if err := os.WriteFile(filepath.Join(m.Root, "/sys/bus/pci/devices/0000:01:00.0/power/runtime_status"), []byte("active\n"), 0o644); err != nil {
t.Fatal(err)
}
m.Run = func(_ context.Context, name string, args ...string) (string, error) {
if name != "nvidia-smi" || args[len(args)-1] != "0000:01:00.0" {
t.Fatalf("ran %s %v", name, args)
}
return "12.50\n", nil
}
if w := m.gpuWatts(context.Background()); w == nil || *w != 42.5 {
t.Fatalf("%v", w)
}
cpu, gpu := 10.0, 42.5
if got := (Draw{CPU: &cpu, GPU: &gpu}).Line(); got != "52.5 W CPU+GPU" {
t.Fatalf("%q", got)
}
}
func TestNothingToMeasureIsSaid(t *testing.T) {
m := tree(t, map[string]string{"/sys/class/power_supply/ACAD/type": "Mains"})
if got := m.Sample(context.Background(), time.Second, noWait).Line(); got != "power not measured" {
t.Fatalf("%q", got)
}
}
func TestOnlyTheRateIsPublishedReadableByAnyone(t *testing.T) {
m := tree(t, nil)
if err := m.Publish("8.0 W CPU"); err != nil {
t.Fatal(err)
}
info, err := os.Stat(filepath.Join(m.Root, DrawFile))
if err != nil || info.Mode().Perm() != 0o644 {
t.Fatalf("%v %v", info, err)
}
b, _ := os.ReadFile(filepath.Join(m.Root, DrawFile))
if string(b) != "8.0 W CPU\n" {
t.Fatalf("%q", b)
}
}