package platform import ( "context" "runtime" "testing" ) func TestDetectGPULinuxGlxinfo(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "glxinfo": {{output: "OpenGL vendor string: NVIDIA Corporation\nDevice: NVIDIA GeForce RTX 3080 (0x2206)\n"}}, }} info := detectGPULinuxWithRunner(runner) if len(info.Models) != 1 || info.Models[0] != "NVIDIA GeForce RTX 3080" { t.Fatalf("Models = %v, want [NVIDIA GeForce RTX 3080]", info.Models) } if info.Brand != "nvidia" { t.Errorf("Brand = %q, want nvidia", info.Brand) } if len(info.Brands) != 1 || info.Brands[0] != "nvidia" { t.Errorf("Brands = %v, want [nvidia]", info.Brands) } if !info.SupportsNV { t.Error("SupportsNV = false, want true for an NVIDIA GPU") } if info.SupportsVA { t.Error("SupportsVA = true, want false for an NVIDIA-only system") } // Every independent enumeration source runs so a display-attached GPU does // not hide a headless/discrete adapter. if calls := runner.called(); len(calls) != 3 || calls[0].name != "glxinfo" || calls[1].name != "vulkaninfo" || calls[2].name != "lspci" { t.Fatalf("probe calls = %+v, want glxinfo, vulkaninfo, lspci", calls) } } func TestDetectGPULinuxLspciFallback(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "glxinfo": {{err: context.DeadlineExceeded}}, "vulkaninfo": {{err: context.DeadlineExceeded}}, "lspci": {{output: "00:00.0 Host bridge: Intel Corporation Device 9a14\n" + "00:02.0 VGA compatible controller: Intel Corporation Alder Lake-P GT2 [Iris Xe Graphics]\n" + "03:00.0 VGA compatible controller: NVIDIA Corporation GA106 [GeForce RTX 3060]\n" + "04:00.1 Audio device: NVIDIA Corporation Device 228e\n"}}, }} info := detectGPULinuxWithRunner(runner) if len(info.Models) != 2 { t.Fatalf("Models = %v, want the two VGA controllers", info.Models) } if len(info.Brands) != 2 { t.Fatalf("Brands = %v, want one brand per model", info.Brands) } // The collapsed brand prefers the discrete NVIDIA GPU. if info.Brand != "nvidia" { t.Errorf("Brand = %q, want nvidia", info.Brand) } if !info.SupportsNV { t.Error("SupportsNV = false, want true") } calls := runner.called() if len(calls) != 3 || calls[2].name != "lspci" { t.Fatalf("probe calls = %+v, want glxinfo, vulkaninfo, lspci", calls) } } func TestDetectGPULinuxAllProbesFail(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "glxinfo": {{err: context.DeadlineExceeded}}, "vulkaninfo": {{err: context.DeadlineExceeded}}, "lspci": {{err: context.DeadlineExceeded}}, }} // External probe failures may still resolve adapters from /sys/class/drm. // The host-specific fallback must remain internally consistent. info := detectGPULinuxWithRunner(runner) if len(info.Brands) != len(info.Models) { t.Errorf("Models/Brands = %v/%v, want parallel slices", info.Models, info.Brands) } if len(info.Models) == 0 && (info.SupportsVA || info.SupportsNV) { t.Error("no acceleration API must be reported without a detected adapter") } } func TestDetectGPULinuxMultiModelBrands(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "glxinfo": {{output: "Device: AMD Radeon(TM) 780M (0x15bf)\nDevice: NVIDIA GeForce RTX 3060 (0x2503)\n"}}, }} info := detectGPULinuxWithRunner(runner) if len(info.Models) != 2 || len(info.Brands) != 2 { t.Fatalf("Models/Brands = %v/%v, want two entries each", info.Models, info.Brands) } if info.Brands[0] != "amd" || info.Brands[1] != "nvidia" { t.Errorf("Brands = %v, want [amd nvidia]", info.Brands) } if !info.SupportsVA || !info.SupportsNV { t.Errorf("hybrid acceleration flags = VA:%v NV:%v, want both true", info.SupportsVA, info.SupportsNV) } } func TestDetectGPULinuxAggregatesAllProbeSources(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "glxinfo": {{output: "Device: AMD Radeon(TM) 780M (0x15bf)\n"}}, "vulkaninfo": {{output: "deviceName = AMD Radeon(TM) 780M (RADV PHOENIX)\ndeviceName = NVIDIA GeForce RTX 3060\n"}}, "lspci": {{output: "03:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA106 [GeForce RTX 3060] [10de:2503] (rev a1)\n"}}, }} info := detectGPULinuxWithRunner(runner) if len(info.Models) != 2 { t.Fatalf("Models = %v, want two deduplicated adapters", info.Models) } if info.Models[0] != "AMD Radeon(TM) 780M" { t.Errorf("AMD model was truncated: %v", info.Models) } if !info.SupportsVA || !info.SupportsNV { t.Errorf("hybrid acceleration flags = VA:%v NV:%v, want both", info.SupportsVA, info.SupportsNV) } } func TestParseCodecSupportLinuxVainfo(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "vainfo": {{output: "VAProfileH264Main : VAEntrypointVLD\nVAProfileHEVCMain : VAEntrypointVLD\n"}}, }} codecs, err := parseCodecSupportLinuxWithRunner(&GPUInfo{}, runner) if err != nil { t.Fatal(err) } if !setsEqual(codecs, []string{"avc", "hevc"}) { t.Errorf("codecs = %v, want [avc hevc]", codecs) } if calls := runner.called(); len(calls) != 1 { t.Fatalf("probe calls = %+v, want only vainfo", calls) } } func TestParseCodecSupportLinuxGPUDatabaseFallback(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "vainfo": {{err: context.DeadlineExceeded}}, "vdpauinfo": {{err: context.DeadlineExceeded}}, }} info := &GPUInfo{Models: []string{"NVIDIA GeForce RTX 3080"}, Brand: "nvidia"} codecs, err := parseCodecSupportLinuxWithRunner(info, runner) if err != nil { t.Fatal(err) } if !contains(codecs, "avc") || !contains(codecs, "hevc") { t.Errorf("codecs = %v, want GPU database codecs for an RTX 3080", codecs) } } func TestParseCodecSupportLinuxCombinesHybridDatabaseMatches(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "vainfo": {{err: context.DeadlineExceeded}}, "vdpauinfo": {{err: context.DeadlineExceeded}}, }} info := &GPUInfo{ Models: []string{"AMD Radeon RX 580", "NVIDIA GeForce RTX 3080"}, Brands: []string{"amd", "nvidia"}, Brand: "nvidia", } codecs, err := parseCodecSupportLinuxWithRunner(info, runner) if err != nil { t.Fatal(err) } for _, want := range []string{"avc", "hevc", "vp9", "av1"} { if !contains(codecs, want) { t.Errorf("combined codecs = %v, missing %q", codecs, want) } } } func TestParseCodecSupportLinuxUnknown(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "vainfo": {{err: context.DeadlineExceeded}}, "vdpauinfo": {{err: context.DeadlineExceeded}}, }} t.Run("unknown GPU model", func(t *testing.T) { info := &GPUInfo{Models: []string{"Mystery GPU 9000"}, Brand: ""} codecs, err := parseCodecSupportLinuxWithRunner(info, runner) if err != nil { t.Fatal(err) } if !setsEqual(codecs, []string{"unknown"}) { t.Errorf("codecs = %v, want [unknown]", codecs) } }) t.Run("no models at all", func(t *testing.T) { codecs, err := parseCodecSupportLinuxWithRunner(&GPUInfo{}, runner) if err != nil { t.Fatal(err) } if !setsEqual(codecs, []string{"unknown"}) { t.Errorf("codecs = %v, want [unknown]", codecs) } }) } func TestParseCodecSupportWindowsWithRunner(t *testing.T) { t.Run("probe failure propagates the error", func(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "powershell": {{err: context.DeadlineExceeded}}, }} _, err := parseCodecSupportWindowsWithRunner(&GPUInfo{Models: []string{"NVIDIA GeForce RTX 3080"}, Brand: "nvidia"}, runner) if err == nil { t.Error("expected the probe error to propagate") } }) t.Run("known model maps to database codecs", func(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "powershell": {{output: "NVIDIA GeForce RTX 3080\n"}}, }} codecs, err := parseCodecSupportWindowsWithRunner(&GPUInfo{Models: []string{"NVIDIA GeForce RTX 3080"}, Brand: "nvidia"}, runner) if err != nil { t.Fatal(err) } if !contains(codecs, "avc") { t.Errorf("codecs = %v, want database codecs", codecs) } }) t.Run("unknown model falls back to unknown", func(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "powershell": {{output: "Mystery GPU 9000\n"}}, }} codecs, err := parseCodecSupportWindowsWithRunner(&GPUInfo{Models: []string{"Mystery GPU 9000"}, Brand: ""}, runner) if err != nil { t.Fatal(err) } if !setsEqual(codecs, []string{"unknown"}) { t.Errorf("codecs = %v, want [unknown]", codecs) } }) } func TestDetectGPUWindowsWmic(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "wmic": {{output: "Name\r\nIntel UHD Graphics 770\r\n\r\n"}}, }} info := detectGPUWindowsWithRunner(runner) if len(info.Models) != 1 || info.Models[0] != "Intel UHD Graphics 770" { t.Fatalf("Models = %v, want [Intel UHD Graphics 770]", info.Models) } if info.Brand != "intel" { t.Errorf("Brand = %q, want intel", info.Brand) } if !info.SupportsVA { t.Error("SupportsVA = false, want true for Intel") } if calls := runner.called(); len(calls) != 1 { t.Fatalf("probe calls = %+v, want only wmic", calls) } } func TestDetectGPUDarwinWithRunner(t *testing.T) { if runtime.GOOS == "darwin" { t.Skip("covered natively on darwin") } t.Run("system_profiler JSON", func(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "system_profiler": {{output: `{"SPDisplaysDataType": [{"_name": "Apple M4"}, {"_name": ""}]}`}}, }} info := detectGPUDarwinWithRunner(runner) if info.Brand != "apple" || !info.SupportsVT { t.Errorf("Brand/SupportsVT = %q/%v, want apple/true", info.Brand, info.SupportsVT) } // On linux/amd64 GOARCH != arm64, so sysctl is skipped and // system_profiler JSON provides the model. if len(info.Models) != 1 || info.Models[0] != "Apple M4" { t.Errorf("Models = %v, want [Apple M4]", info.Models) } }) t.Run("probe failure leaves defaults", func(t *testing.T) { runner := &fakeProbeRunner{responses: map[string][]fakeProbeResponse{ "system_profiler": {{err: context.DeadlineExceeded}}, }} info := detectGPUDarwinWithRunner(runner) if len(info.Models) != 0 { t.Errorf("Models = %v, want empty", info.Models) } if info.Brand != "apple" { t.Errorf("Brand = %q, want apple", info.Brand) } }) }