193 lines
7.4 KiB
Go
193 lines
7.4 KiB
Go
package platform
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestClassifyGPUFromVendorClass(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
vendor string
|
|
class string
|
|
wantIsGPU bool
|
|
wantVendor string
|
|
}{
|
|
{"nvidia GPU", "0x10de", "0x030200", true, "nvidia"},
|
|
{"amd GPU", "0x1002", "0x030000", true, "amd"},
|
|
{"nvidia NIC (same vendor, not display class)", "0x10de", "0x020000", false, ""},
|
|
{"intel NIC", "0x8086", "0x020000", false, ""},
|
|
{"unrelated display-class vendor", "0x1234", "0x030000", false, ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotIsGPU, gotVendor := classifyGPUFromVendorClass(tc.vendor, tc.class)
|
|
if gotIsGPU != tc.wantIsGPU || gotVendor != tc.wantVendor {
|
|
t.Fatalf("classifyGPUFromVendorClass(%q,%q) = (%v,%q), want (%v,%q)",
|
|
tc.vendor, tc.class, gotIsGPU, gotVendor, tc.wantIsGPU, tc.wantVendor)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPCIeBridgeUsesDownstreamCapability(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
portSpeed string
|
|
portWidth int
|
|
endpointSpeed string
|
|
endpointWidth int
|
|
wantSpeed string
|
|
wantWidth int
|
|
}{
|
|
{"ConnectX-5 behind Gen4 root port", "Gen4", 16, "Gen3", 8, "Gen3", 8},
|
|
{"Adaptec SAS behind Gen4 root port", "Gen4", 16, "Gen3", 8, "Gen3", 8},
|
|
{"I350 behind Gen4 root port", "Gen4", 16, "Gen2", 4, "Gen2", 4},
|
|
{"faster endpoint remains port-limited", "Gen3", 8, "Gen4", 16, "Gen3", 8},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
gotSpeed := minPCIeLinkSpeed(tc.portSpeed, tc.endpointSpeed)
|
|
gotWidth := minPositiveInt(tc.portWidth, tc.endpointWidth)
|
|
if gotSpeed != tc.wantSpeed || gotWidth != tc.wantWidth {
|
|
t.Fatalf("effective capability = %s x%d, want %s x%d", gotSpeed, gotWidth, tc.wantSpeed, tc.wantWidth)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPCIeBridgeAtEndpointMaximumPasses(t *testing.T) {
|
|
maxSpeed := minPCIeLinkSpeed("Gen4", "Gen3")
|
|
maxWidth := minPositiveInt(16, 8)
|
|
finding := pcieLinkFinding{
|
|
BDF: "0000:4a:02.0",
|
|
Description: "Intel root port to ConnectX-5",
|
|
BeforeSpeed: "Gen3",
|
|
AfterSpeed: "Gen3",
|
|
MaxSpeed: maxSpeed,
|
|
PortMaxSpeed: "Gen4",
|
|
Width: 8,
|
|
MaxWidth: maxWidth,
|
|
PortMaxWidth: 16,
|
|
Degraded: "Gen3" != maxSpeed,
|
|
}
|
|
|
|
summary := renderPCIeLinkCheckSummary([]pcieLinkFinding{finding})
|
|
if !strings.Contains(summary, "overall_status=OK") {
|
|
t.Fatalf("endpoint-limited bridge should pass, got:\n%s", summary)
|
|
}
|
|
report := renderPCIeLinkCheckReport([]pcieLinkFinding{finding})
|
|
if !strings.Contains(report, "limited by downstream device") {
|
|
t.Fatalf("report should explain the effective maximum, got:\n%s", report)
|
|
}
|
|
}
|
|
|
|
func TestDownstreamPCIeLinkCapabilityFromSysfsTopology(t *testing.T) {
|
|
bridgeDir := t.TempDir()
|
|
childDir := filepath.Join(bridgeDir, "0000:4b:00.0")
|
|
if err := os.Mkdir(childDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(childDir, "max_link_speed"), []byte("8.0 GT/s PCIe\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(childDir, "max_link_width"), []byte("8\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A non-BDF sysfs entry must not influence peer discovery.
|
|
if err := os.WriteFile(filepath.Join(bridgeDir, "max_link_speed"), []byte("16.0 GT/s PCIe\n"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
speed, width, ok := downstreamPCIeLinkCapabilityAt(bridgeDir)
|
|
if !ok || speed != "Gen3" || width != 8 {
|
|
t.Fatalf("downstream capability = (%q, %d, %v), want (Gen3, 8, true)", speed, width, ok)
|
|
}
|
|
}
|
|
|
|
func TestRenderPCIeLinkCheckSummaryDegradedGPU(t *testing.T) {
|
|
findings := []pcieLinkFinding{
|
|
{BDF: "0000:0d:00.0", Description: "NVIDIA GPU", IsGPU: true, GPUVendor: "nvidia",
|
|
BeforeSpeed: "Gen1", AfterSpeed: "Gen1", MaxSpeed: "Gen5", Degraded: true},
|
|
{BDF: "0000:37:00.0", Description: "NVIDIA GPU", IsGPU: true, GPUVendor: "nvidia",
|
|
BeforeSpeed: "Gen1", AfterSpeed: "Gen5", MaxSpeed: "Gen5", Degraded: false},
|
|
{BDF: "0000:01:00.0", Description: "Mellanox NIC", IsGPU: false,
|
|
BeforeSpeed: "Gen3", AfterSpeed: "Gen4", MaxSpeed: "Gen4", Degraded: false},
|
|
}
|
|
summary := renderPCIeLinkCheckSummary(findings)
|
|
if !strings.Contains(summary, "gpu_nvidia_status=FAILED") {
|
|
t.Fatalf("expected gpu_nvidia_status=FAILED (one degraded GPU should fail the vendor group), got:\n%s", summary)
|
|
}
|
|
if !strings.Contains(summary, "other_status=OK") {
|
|
t.Fatalf("expected other_status=OK, got:\n%s", summary)
|
|
}
|
|
if !strings.Contains(summary, "overall_status=FAILED") {
|
|
t.Fatalf("expected overall_status=FAILED, got:\n%s", summary)
|
|
}
|
|
}
|
|
|
|
func TestRenderPCIeLinkCheckSummaryAllClean(t *testing.T) {
|
|
findings := []pcieLinkFinding{
|
|
{BDF: "0000:0d:00.0", IsGPU: true, GPUVendor: "nvidia", BeforeSpeed: "Gen1", AfterSpeed: "Gen5", MaxSpeed: "Gen5"},
|
|
{BDF: "0000:2c:00.0", Skipped: "device disabled (no data traffic; link state has no operational impact)"},
|
|
}
|
|
summary := renderPCIeLinkCheckSummary(findings)
|
|
if !strings.Contains(summary, "overall_status=OK") {
|
|
t.Fatalf("expected overall_status=OK, got:\n%s", summary)
|
|
}
|
|
if strings.Contains(summary, "other_status=") {
|
|
t.Fatalf("disabled/skipped-only device should not produce an other_status line, got:\n%s", summary)
|
|
}
|
|
}
|
|
|
|
func TestRenderPCIeLinkCheckSummaryEmptySlotDoesNotFail(t *testing.T) {
|
|
// An unpopulated switch downstream port (no card ever seated) is a
|
|
// normal, common configuration — it must not fail the SAT the way a
|
|
// genuinely degraded link does.
|
|
findings := []pcieLinkFinding{
|
|
{BDF: "0000:0d:00.0", IsGPU: true, GPUVendor: "nvidia", BeforeSpeed: "Gen5", AfterSpeed: "Gen5", MaxSpeed: "Gen5"},
|
|
{BDF: "0000:2a:00.0", Description: "PEX890xx PCIe Gen 5 Switch",
|
|
NotPresent: true, Skipped: "no device present downstream (empty slot/riser — nothing to retrain)"},
|
|
}
|
|
summary := renderPCIeLinkCheckSummary(findings)
|
|
if !strings.Contains(summary, "overall_status=OK") {
|
|
t.Fatalf("expected overall_status=OK for an empty slot, got:\n%s", summary)
|
|
}
|
|
if strings.Contains(summary, "other_status=") {
|
|
t.Fatalf("empty-slot-only device should not produce an other_status line, got:\n%s", summary)
|
|
}
|
|
|
|
report := renderPCIeLinkCheckReport(findings)
|
|
if !strings.Contains(report, "skipped: no device present downstream") {
|
|
t.Fatalf("expected empty slot to be reported as skipped, not degraded, got:\n%s", report)
|
|
}
|
|
if strings.Contains(report, "retrained to") {
|
|
t.Fatalf("empty slot must not be reported with a fabricated retrain speed, got:\n%s", report)
|
|
}
|
|
}
|
|
|
|
func TestRenderPCIeLinkCheckSummaryDeviceFellOffBusStillFails(t *testing.T) {
|
|
// A device that had a live link before the retrain and is gone right
|
|
// after it is a real regression, distinct from a slot that was never
|
|
// populated — this must still fail the check.
|
|
findings := []pcieLinkFinding{
|
|
{BDF: "0000:ab:00.0", Description: "PEX890xx PCIe Gen 5 Switch",
|
|
BeforeSpeed: "Gen5", AfterSpeed: "Gen5", MaxSpeed: "Gen5", Width: 0, NotPresent: true, Degraded: true},
|
|
}
|
|
summary := renderPCIeLinkCheckSummary(findings)
|
|
if !strings.Contains(summary, "overall_status=FAILED") {
|
|
t.Fatalf("expected overall_status=FAILED when a device disappears after retrain, got:\n%s", summary)
|
|
}
|
|
if !strings.Contains(summary, "no device detected downstream") {
|
|
t.Fatalf("expected an honest 'no device detected' reason, not a fabricated speed, got:\n%s", summary)
|
|
}
|
|
|
|
report := renderPCIeLinkCheckReport(findings)
|
|
if !strings.Contains(report, "FELL OFF BUS") {
|
|
t.Fatalf("expected FELL OFF BUS verdict in report, got:\n%s", report)
|
|
}
|
|
}
|