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) } }