package platform import ( "os" "path/filepath" "strings" "testing" ) func TestParseGlxInfo(t *testing.T) { tests := []struct { name string output string want []string }{ { name: "device line", output: "OpenGL vendor string: NVIDIA Corporation\n" + "Device: NVIDIA GeForce RTX 3080 (0x2206)\n", want: []string{"NVIDIA GeForce RTX 3080"}, }, { name: "renderer string fallback", output: "OpenGL renderer string: AMD Radeon RX 6800 XT (navi21, LLVM 15.0.7)\n" + "OpenGL version string: 4.6\n", want: []string{"AMD Radeon RX 6800 XT"}, }, { name: "duplicate models are deduplicated", output: "Device: Intel UHD Graphics 770 (0x4680)\n" + "OpenGL renderer string: Intel UHD Graphics 770 (0x4680)\n", want: []string{"Intel UHD Graphics 770"}, }, { name: "multi gpu system", output: "Device: AMD Radeon(TM) 780M (0x15bf)\n" + "Device: NVIDIA GeForce RTX 3060 (0x2503)\n", want: []string{"AMD Radeon(TM) 780M", "NVIDIA GeForce RTX 3060"}, }, { name: "unrelated output", output: "OpenGL vendor string: Mesa\nOpenGL version string: 4.5\n", want: nil, }, { name: "empty output", output: "", want: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseGlxInfo(tt.output) if len(got) != len(tt.want) { t.Fatalf("parseGlxInfo() = %v, want %v", got, tt.want) } for i, model := range tt.want { if got[i] != model { t.Errorf("parseGlxInfo()[%d] = %q, want %q", i, got[i], model) } } }) } } // TestLookupGPUCodecsMatchPasses exercises each of the four matcher passes // (model, series, codename, alt codename) against a minimal fixture table. func TestLookupGPUCodecsMatchPasses(t *testing.T) { fixture := []GPUModelEntry{ { ModelNames: []string{"Example Pro 1000"}, SeriesNames: []string{"Example Series"}, Codename: "Exemplum 10", AltCodenames: []string{"EX100", "EX101"}, Codecs: []string{"avc", "hevc"}, }, } tests := []struct { name string gpu string matched bool }{ {"model name pass", "SomeVendor Example Pro 1000 OC", true}, {"series pass", "Example Series 16GB", true}, {"codename pass", "Exemplum 10 LE", true}, {"alt codename pass", "EX100 Refresh", true}, {"case and trademark normalization", "EXAMPLE(TM) PRO 1000", true}, {"no match", "Totally Different GPU", false}, {"empty name", "", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { codecs, matched := lookupGPUCodecs(tt.gpu, fixture) if matched != tt.matched { t.Fatalf("lookupGPUCodecs(%q) matched = %v, want %v", tt.gpu, matched, tt.matched) } if !tt.matched { if len(codecs) != 1 || codecs[0] != "unknown" { t.Errorf("lookupGPUCodecs(%q) codecs = %v, want [unknown]", tt.gpu, codecs) } return } if !setsEqual(codecs, []string{"avc", "hevc"}) { t.Errorf("lookupGPUCodecs(%q) codecs = %v, want [avc hevc]", tt.gpu, codecs) } }) } } func TestLookupGPUCodecsWithFallbackBrands(t *testing.T) { tests := []struct { name string gpu string brand string matched bool codec string // a codec that must be present when matched }{ {"brand hit stays in-brand", "NVIDIA GeForce RTX 3080", "nvidia", true, "avc"}, {"wrong brand falls back cross-brand", "NVIDIA GeForce RTX 3080", "amd", true, "avc"}, {"empty brand searches all tables", "Intel Arc A770", "", true, "av1"}, {"uppercase brand matches", "AMD Radeon RX 6800 XT", "AMD", true, "av1"}, {"unknown gpu", "Mali-G71", "arm", false, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { codecs, matched := lookupGPUCodecsWithFallback(tt.gpu, tt.brand) if matched != tt.matched { t.Fatalf("lookupGPUCodecsWithFallback(%q, %q) matched = %v, want %v", tt.gpu, tt.brand, matched, tt.matched) } if tt.matched && !contains(codecs, tt.codec) { t.Errorf("lookupGPUCodecsWithFallback(%q, %q) = %v, missing %q", tt.gpu, tt.brand, codecs, tt.codec) } if !tt.matched && !setsEqual(codecs, []string{"unknown"}) { t.Errorf("lookupGPUCodecsWithFallback(%q, %q) = %v, want [unknown]", tt.gpu, tt.brand, codecs) } }) } } func TestGetHwaDescription(t *testing.T) { tests := []struct { method string want string }{ {"auto", "Let MPV choose best method"}, {"auto-safe", "Safe automatic selection (NVIDIA only)"}, {"nvdec", "NVIDIA Video Decode"}, {"nvdec-copy", "NVIDIA Video Decode with system RAM copy"}, {"vaapi", "Video Acceleration API (Linux/AMD/Intel)"}, {"vaapi-copy", "Video Acceleration API with system RAM copy"}, {"vulkan", "Vulkan video decoding"}, {"vulkan-copy", "Vulkan with system RAM copy"}, {"d3d11va", "Direct3D 11 Video Acceleration (Windows)"}, {"d3d11va-copy", "Direct3D 11 Video Acceleration with system RAM copy"}, {"videotoolbox", "Apple VideoToolbox (macOS)"}, {"videotoolbox-copy", "Apple VideoToolbox with system RAM copy"}, {"drm", "Direct Rendering Manager (Linux/AMD/Intel)"}, {"drm-copy", "Direct Rendering Manager with system RAM copy"}, {"no", "Disable hardware acceleration (CPU only)"}, // ,auto suffix appends the fallback note to the base description {"nvdec,auto", "NVIDIA Video Decode (fallback to auto)"}, {"vaapi,auto", "Video Acceleration API (Linux/AMD/Intel) (fallback to auto)"}, // Matching is case-insensitive {"NVDEC", "NVIDIA Video Decode"}, {"NVDEC,AUTO", "NVIDIA Video Decode (fallback to auto)"}, // Unknown methods pass through unchanged for forward compatibility {"future-api", "future-api"}, } for _, tt := range tests { t.Run(tt.method, func(t *testing.T) { if got := GetHwaDescription(tt.method); got != tt.want { t.Errorf("GetHwaDescription(%q) = %q, want %q", tt.method, got, tt.want) } }) } } func TestParseVainfo(t *testing.T) { tests := []struct { name string output string want []string wantErr bool }{ { name: "VLD entrypoints map to codecs", output: "vainfo: VA-API version: 1.20 (libva 2.20.0)\n" + "VAProfileH264Main : VAEntrypointVLD\n" + "VAProfileHEVCMain : VAEntrypointVLD\n" + "VAProfileVP9Profile0 : VAEntrypointVLD\n" + "VAProfileAV1Profile0 : VAEntrypointVLD\n" + "VAProfileMPEG2Main : VAEntrypointVLD\n", want: []string{"avc", "hevc", "vp9", "av1", "mpeg2"}, }, { name: "non-VLD entrypoints are ignored", output: "VAProfileH264Main : VAEntrypointEncSlice\n" + "VAProfileHEVCMain : VAEntrypointFEI\n", wantErr: true, }, { name: "duplicate profiles collapse to one codec", output: "VAProfileH264Main : VAEntrypointVLD\n" + "VAProfileH264High : VAEntrypointVLD\n" + "VAProfileH264ConstrainedBaseline: VAEntrypointVLD\n", want: []string{"avc"}, }, { name: "empty output", output: "", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := parseVainfo(tt.output) if (err != nil) != tt.wantErr { t.Fatalf("parseVainfo() error = %v, wantErr %v", err, tt.wantErr) } if tt.wantErr { return } if !setsEqual(got, tt.want) { t.Errorf("parseVainfo() = %v, want %v", got, tt.want) } }) } } func TestParseVDPAUInfo(t *testing.T) { t.Run("decoder capabilities map to codecs", func(t *testing.T) { output := "Decoder capabilities:\n" + "MPEG1 16 4096 4096\n" + "H264 16 8192 8192\n" + "HEVC_MAIN 16 8192 8192\n" + "VP9_PROFILE_0 16 8192 8192\n" + "AV1_MAIN 16 8192 8192\n" got, err := parseVDPAUInfo(output) if err != nil { t.Fatal(err) } for _, want := range []string{"mpeg2", "avc", "hevc", "vp9", "av1"} { if !contains(got, want) { t.Errorf("parseVDPAUInfo() = %v, missing %q", got, want) } } }) t.Run("no decoder lines is an error", func(t *testing.T) { if _, err := parseVDPAUInfo("vdpauinfo: cannot connect to X server\n"); err == nil { t.Error("parseVDPAUInfo() with no codecs must return an error") } }) } func TestFileExists(t *testing.T) { existing := filepath.Join(t.TempDir(), "present") if err := os.WriteFile(existing, []byte("x"), 0644); err != nil { t.Fatal(err) } if !fileExists(existing) { t.Errorf("fileExists(%q) = false, want true", existing) } if fileExists(filepath.Join(t.TempDir(), "missing")) { t.Error("fileExists() = true for a missing path, want false") } } func TestGetGPUModelFromPCIID(t *testing.T) { if got := getGPUModelFromPCIID("1002:744C"); !strings.Contains(strings.ToLower(got), "amd") && !strings.Contains(strings.ToLower(got), "advanced micro devices") { t.Errorf("getGPUModelFromPCIID() = %q, want an AMD model or vendor fallback", got) } if got := getGPUModelFromPCIID(""); got != "" { t.Errorf("getGPUModelFromPCIID(\"\") = %q, want empty", got) } } func TestParsePCIIDsModel(t *testing.T) { database := "# pci.ids fixture\n1002 Advanced Micro Devices, Inc. [AMD/ATI]\n\t744c Navi 31 [Radeon RX 7900 XT/7900 XTX]\n\t\t1da2 e471 Subsystem\n10de NVIDIA Corporation\n\t2503 GA106 [GeForce RTX 3060]\n" if got := parsePCIIDsModel(database, "1002", "744c"); got != "Advanced Micro Devices, Inc. [AMD/ATI] Navi 31 [Radeon RX 7900 XT/7900 XTX]" { t.Fatalf("AMD model = %q", got) } if got := parsePCIIDsModel(database, "10de", "2503"); got != "NVIDIA Corporation GA106 [GeForce RTX 3060]" { t.Fatalf("NVIDIA model = %q", got) } if got := parsePCIIDsModel(database, "8086", "ffff"); got != "" { t.Fatalf("unknown model = %q, want empty", got) } } func TestDRMCardAndPCIIDParsing(t *testing.T) { for _, name := range []string{"card0", "card12"} { if !isDRMCardName(name) { t.Errorf("isDRMCardName(%q) = false", name) } } for _, name := range []string{"card", "card0-DP-1", "renderD128", "cardx"} { if isDRMCardName(name) { t.Errorf("isDRMCardName(%q) = true", name) } } vendor, device, ok := splitPCIID("PCI_ID=1002:744C") if !ok || vendor != "1002" || device != "744c" { t.Fatalf("splitPCIID() = %q/%q/%v", vendor, device, ok) } }