Modules talk over the bus. The power module's bundle puts each fresh reading of its own sampler in its state draw, keyed by the machine; the bar's block shows that state, which the bar's bundle watches under the read the mesh grants it. A new catalogue check refuses a module naming another module's path under /run or /var/lib.
211 lines
7.8 KiB
Go
211 lines
7.8 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")
|
|
// 1 J over 50 ms: 20 W, well inside what a package draws.
|
|
d := m.Sample(context.Background(), time.Second, func(time.Duration) bool {
|
|
time.Sleep(50 * time.Millisecond)
|
|
return os.WriteFile(counter, []byte("2000000\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 TestOnlyTheRateIsLeftForTheBundle(t *testing.T) {
|
|
m := tree(t, nil)
|
|
w := 8.0
|
|
if err := m.Publish(Draw{CPU: &w}.Reading(time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC))); 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) != `{"text":"8.0 W CPU","watts":8,"measures":"cpu","at":"2026-10-08T12:00:00Z"}`+"\n" {
|
|
t.Fatalf("%q", b)
|
|
}
|
|
}
|
|
|
|
func TestAPackageSeenTwiceIsCountedOnce(t *testing.T) {
|
|
m := tree(t, map[string]string{
|
|
"/sys/class/powercap/intel-rapl:0/name": "package-0",
|
|
"/sys/class/powercap/intel-rapl:0/energy_uj": "100",
|
|
"/sys/class/powercap/intel-rapl-mmio:0/name": "package-0",
|
|
"/sys/class/powercap/intel-rapl-mmio:0/energy_uj": "999",
|
|
"/sys/class/powercap/intel-rapl:1/name": "package-1",
|
|
"/sys/class/powercap/intel-rapl:1/energy_uj": "5",
|
|
})
|
|
got := m.packageCounters()
|
|
if len(got) != 2 || got["package-0"].uj != 100 || got["package-1"].uj != 5 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
}
|
|
|
|
func TestACounterResetGivesNoReading(t *testing.T) {
|
|
// After a resume the counter starts again near nothing: across its range that would be thousands
|
|
// of watts, which is no reading at all.
|
|
before := map[string]counter{"package-0": {uj: 40_000_000_000, max: 65_532_610_987}}
|
|
after := map[string]counter{"package-0": {uj: 1_000_000, max: 65_532_610_987}}
|
|
if w := cpuWatts(before, after, 5*time.Second); w != nil {
|
|
t.Fatalf("a reset counter read as %.0f W", *w)
|
|
}
|
|
// A true wrap, near the end of the range, is still counted.
|
|
before = map[string]counter{"package-0": {uj: 65_532_610_987 - 10_000_000, max: 65_532_610_987}}
|
|
after = map[string]counter{"package-0": {uj: 40_000_000, max: 65_532_610_987}}
|
|
if w := cpuWatts(before, after, 5*time.Second); w == nil || *w != 10 {
|
|
t.Fatalf("a wrapped counter read as %v", w)
|
|
}
|
|
}
|
|
|
|
func TestASleepingAMDCardIsNotRead(t *testing.T) {
|
|
m := tree(t, map[string]string{
|
|
"/sys/class/drm/card0/device/mem_info_vram_vendor": "samsung",
|
|
"/sys/class/drm/card0/device/power/runtime_status": "suspended",
|
|
"/sys/class/drm/card0/device/hwmon/hwmon3/power1_average": "30000000",
|
|
})
|
|
if w := m.gpuWatts(context.Background()); w != nil {
|
|
t.Fatalf("a suspended card was read: %v", *w)
|
|
}
|
|
}
|
|
|
|
func TestTheLoopPublishesEveryIntervalAndStopsWhenCancelled(t *testing.T) {
|
|
m := tree(t, map[string]string{
|
|
"/sys/class/power_supply/BAT0/type": "Battery",
|
|
"/sys/class/power_supply/BAT0/status": "Discharging",
|
|
"/sys/class/power_supply/BAT0/power_now": "12000000",
|
|
})
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
var lines []string
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- runDraw(ctx, m, 20*time.Millisecond, func(r Reading) error { lines = append(lines, r.Text); return nil })
|
|
}()
|
|
time.Sleep(210 * time.Millisecond)
|
|
cancel()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("the loop did not stop when cancelled")
|
|
}
|
|
// One at once, then one every 20 ms: about eleven in 210 ms, never a burst, never a stall.
|
|
if len(lines) < 6 || len(lines) > 12 || lines[0] != "12.0 W machine" {
|
|
t.Fatalf("%d lines: %v", len(lines), lines)
|
|
}
|
|
}
|