Reads the same PDH counters as Task Manager (\GPU Engine(*)\Utilization Percentage, locale-independent via PdhAddEnglishCounterW), which cover graphics work under WDDM — games are caught without an exe list and the offender is named. Windows-only; a missing or failing counter disables the path with one log line. dwm (the desktop compositor) joins the default ignore list.
136 lines
3.6 KiB
Go
136 lines
3.6 KiB
Go
package game
|
|
|
|
import (
|
|
"runtime"
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
func TestNormName(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"Cyberpunk2077.exe": "cyberpunk2077",
|
|
"ollama": "ollama",
|
|
"OLLAMA APP.EXE": "ollama app",
|
|
" python.exe ": "python",
|
|
"hl2": "hl2",
|
|
} {
|
|
if got := normName(in); got != want {
|
|
t.Errorf("normName(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseComputeApps(t *testing.T) {
|
|
apps, err := parseComputeApps("1234, 512\n 42 , 8192 \n\n")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []computeApp{{PID: 1234, UsedMB: 512}, {PID: 42, UsedMB: 8192}}
|
|
if !slices.Equal(apps, want) {
|
|
t.Errorf("got %+v, want %+v", apps, want)
|
|
}
|
|
|
|
// WDDM reports "N/A" for memory; those rows are skipped, not fatal.
|
|
apps, err = parseComputeApps("1234, N/A\n42, 1024\n")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Equal(apps, []computeApp{{PID: 42, UsedMB: 1024}}) {
|
|
t.Errorf("got %+v", apps)
|
|
}
|
|
|
|
if _, err := parseComputeApps("garbage\n"); err == nil {
|
|
t.Error("expected an error for a malformed line")
|
|
}
|
|
}
|
|
|
|
func TestDetect(t *testing.T) {
|
|
d := New([]string{"Cyberpunk2077.exe", "hl2"}, 1024, 0,
|
|
[]string{"ollama", "python", "pythonw"}, nil)
|
|
ps := []Process{
|
|
{PID: 10, Name: "ollama.exe"},
|
|
{PID: 20, Name: "python.exe"},
|
|
{PID: 30, Name: "cyberpunk2077.exe"},
|
|
}
|
|
apps := []computeApp{
|
|
{PID: 10, UsedMB: 8192}, // ignored: ollama
|
|
{PID: 20, UsedMB: 4096}, // ignored: python (ComfyUI)
|
|
{PID: 40, UsedMB: 2048}, // foreign, above threshold
|
|
{PID: 50, UsedMB: 100}, // foreign but below threshold
|
|
}
|
|
holders := d.detect(ps, apps, nil)
|
|
if len(holders) != 2 {
|
|
t.Fatalf("got %v, want 2 holders", holders)
|
|
}
|
|
if holders[0] != "cyberpunk2077.exe (pid 30)" {
|
|
t.Errorf("holders[0] = %q", holders[0])
|
|
}
|
|
if holders[1] != "unknown process (pid 40) using 2048 MiB VRAM" {
|
|
t.Errorf("holders[1] = %q", holders[1])
|
|
}
|
|
}
|
|
|
|
func TestDetectEngineUtil(t *testing.T) {
|
|
d := New(nil, 0, 30, []string{"dwm", "python"}, nil)
|
|
ps := []Process{
|
|
{PID: 10, Name: "dwm.exe"},
|
|
{PID: 20, Name: "game.exe"},
|
|
{PID: 40, Name: "browser.exe"},
|
|
}
|
|
utils := map[int]float64{
|
|
10: 45, // ignored: dwm
|
|
20: 61, // foreign, above threshold
|
|
30: 82, // foreign, unknown name
|
|
40: 5, // below threshold
|
|
}
|
|
holders := d.detect(ps, nil, utils)
|
|
want := []string{
|
|
"game.exe (pid 20) using 61% GPU",
|
|
"unknown process (pid 30) using 82% GPU",
|
|
}
|
|
if !slices.Equal(holders, want) {
|
|
t.Errorf("got %v, want %v", holders, want)
|
|
}
|
|
}
|
|
|
|
func TestDetectNothingConfigured(t *testing.T) {
|
|
d := New(nil, 0, 0, nil, nil)
|
|
if got := d.detect([]Process{{PID: 1, Name: "game.exe"}}, nil, nil); len(got) != 0 {
|
|
t.Errorf("got %v, want none", got)
|
|
}
|
|
}
|
|
|
|
func TestParseGPUEngineInstance(t *testing.T) {
|
|
pid, eng, ok := parseGPUEngineInstance("pid_1234_luid_0x00000000_0x00011A2B_phys_0_eng_0_engtype_3D")
|
|
if !ok || pid != 1234 || eng != "3D" {
|
|
t.Errorf("got %d, %q, %v", pid, eng, ok)
|
|
}
|
|
pid, eng, ok = parseGPUEngineInstance("pid_42_luid_0x0_0x0_phys_0_eng_1_engtype_Copy")
|
|
if !ok || pid != 42 || eng != "Copy" {
|
|
t.Errorf("got %d, %q, %v", pid, eng, ok)
|
|
}
|
|
for _, bad := range []string{"", "something", "pid_", "pid_x_luid", "pid_-1_luid_0"} {
|
|
if _, _, ok := parseGPUEngineInstance(bad); ok {
|
|
t.Errorf("%q parsed, want failure", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProcessesLive(t *testing.T) {
|
|
if runtime.GOOS != "windows" && runtime.GOOS != "linux" {
|
|
t.Skip("no process listing on this platform")
|
|
}
|
|
ps, err := processes()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(ps) == 0 {
|
|
t.Fatal("no processes listed")
|
|
}
|
|
for _, p := range ps {
|
|
if p.Name == "" {
|
|
t.Errorf("pid %d has an empty name", p.PID)
|
|
}
|
|
}
|
|
}
|