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.
138 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|