1980 lines
58 KiB
Go
1980 lines
58 KiB
Go
package exporter
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
)
|
|
|
|
func TestConvertToReanimator(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input *models.AnalysisResult
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "nil result",
|
|
input: nil,
|
|
wantErr: true,
|
|
errMsg: "no data available",
|
|
},
|
|
{
|
|
name: "no hardware",
|
|
input: &models.AnalysisResult{
|
|
Filename: "test.json",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "no hardware data available",
|
|
},
|
|
{
|
|
name: "no board serial",
|
|
input: &models.AnalysisResult{
|
|
Filename: "test.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "board serial_number is required",
|
|
},
|
|
{
|
|
name: "valid minimal data",
|
|
input: &models.AnalysisResult{
|
|
Filename: "test.json",
|
|
SourceType: "api",
|
|
Protocol: "redfish",
|
|
TargetHost: "10.10.10.10",
|
|
CollectedAt: time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC),
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{
|
|
Manufacturer: "Supermicro",
|
|
ProductName: "X12DPG-QT6",
|
|
SerialNumber: "TEST123",
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := ConvertToReanimator(tt.input)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Errorf("expected error containing %q, got nil", tt.errMsg)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
return
|
|
}
|
|
if result == nil {
|
|
t.Error("expected non-nil result")
|
|
return
|
|
}
|
|
if result.Hardware.Board.SerialNumber != tt.input.Hardware.BoardInfo.SerialNumber {
|
|
t.Errorf("board serial mismatch: got %q, want %q",
|
|
result.Hardware.Board.SerialNumber,
|
|
tt.input.Hardware.BoardInfo.SerialNumber)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInferCPUManufacturer(t *testing.T) {
|
|
tests := []struct {
|
|
model string
|
|
want string
|
|
}{
|
|
{"INTEL(R) XEON(R) GOLD 6530", "Intel"},
|
|
{"Intel Core i9-12900K", "Intel"},
|
|
{"AMD EPYC 7763", "AMD"},
|
|
{"AMD Ryzen 9 5950X", "AMD"},
|
|
{"ARM Cortex-A78", "ARM"},
|
|
{"Ampere Altra Max", "Ampere"},
|
|
{"Unknown CPU Model", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.model, func(t *testing.T) {
|
|
got := inferCPUManufacturer(tt.model)
|
|
if got != tt.want {
|
|
t.Errorf("inferCPUManufacturer(%q) = %q, want %q", tt.model, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizedSerial(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
name: "empty",
|
|
in: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "n_a",
|
|
in: "N/A",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "unknown",
|
|
in: "unknown",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "normal",
|
|
in: "SN123",
|
|
want: "SN123",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := normalizedSerial(tt.in)
|
|
if got != tt.want {
|
|
t.Errorf("normalizedSerial() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInferStorageStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stor models.Storage
|
|
want string
|
|
}{
|
|
{
|
|
name: "present",
|
|
stor: models.Storage{
|
|
Present: true,
|
|
},
|
|
want: "Unknown",
|
|
},
|
|
{
|
|
name: "not present",
|
|
stor: models.Storage{
|
|
Present: false,
|
|
},
|
|
want: "Unknown",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := inferStorageStatus(tt.stor)
|
|
if got != tt.want {
|
|
t.Errorf("inferStorageStatus() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeStatus_PassFail(t *testing.T) {
|
|
if got := normalizeStatus("PASS", false); got != "OK" {
|
|
t.Fatalf("expected PASS -> OK, got %q", got)
|
|
}
|
|
if got := normalizeStatus("FAIL", false); got != "Critical" {
|
|
t.Fatalf("expected FAIL -> Critical, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertCPUs(t *testing.T) {
|
|
cpus := []models.CPU{
|
|
{
|
|
Socket: 0,
|
|
Model: "INTEL(R) XEON(R) GOLD 6530",
|
|
Cores: 32,
|
|
Threads: 64,
|
|
FrequencyMHz: 2100,
|
|
MaxFreqMHz: 4000,
|
|
},
|
|
{
|
|
Socket: 1,
|
|
Model: "AMD EPYC 7763",
|
|
Cores: 64,
|
|
Threads: 128,
|
|
FrequencyMHz: 2450,
|
|
MaxFreqMHz: 3500,
|
|
},
|
|
}
|
|
|
|
result := convertCPUs(cpus, "2026-02-10T15:30:00Z")
|
|
|
|
if len(result) != 2 {
|
|
t.Fatalf("expected 2 CPUs, got %d", len(result))
|
|
}
|
|
|
|
if result[0].Manufacturer != "Intel" {
|
|
t.Errorf("expected Intel manufacturer for first CPU, got %q", result[0].Manufacturer)
|
|
}
|
|
|
|
if result[1].Manufacturer != "AMD" {
|
|
t.Errorf("expected AMD manufacturer for second CPU, got %q", result[1].Manufacturer)
|
|
}
|
|
|
|
if result[0].Status != "Unknown" {
|
|
t.Errorf("expected Unknown status, got %q", result[0].Status)
|
|
}
|
|
if result[0].SerialNumber != "" {
|
|
t.Errorf("expected empty CPU serial when source serial is absent, got %q", result[0].SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestConvertMemory(t *testing.T) {
|
|
memory := []models.MemoryDIMM{
|
|
{
|
|
Slot: "CPU0_C0D0",
|
|
Present: true,
|
|
SizeMB: 32768,
|
|
Type: "DDR5",
|
|
SerialNumber: "TEST-MEM-001",
|
|
Status: "OK",
|
|
},
|
|
{
|
|
Slot: "CPU0_C1D0",
|
|
Present: false,
|
|
},
|
|
}
|
|
|
|
result := convertMemory(memory, "2026-02-10T15:30:00Z")
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 populated memory module, got %d", len(result))
|
|
}
|
|
|
|
if result[0].Status != "OK" {
|
|
t.Errorf("expected OK status for first module, got %q", result[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestConvertMemory_KeepsInstalledDIMMWithUnknownSize(t *testing.T) {
|
|
memory := []models.MemoryDIMM{
|
|
{
|
|
Slot: "PROC 1 DIMM 3",
|
|
Present: true,
|
|
SizeMB: 0,
|
|
Manufacturer: "Hynix",
|
|
PartNumber: "HMCG88AEBRA115N",
|
|
SerialNumber: "2B5F92C6",
|
|
Status: "OK",
|
|
},
|
|
}
|
|
|
|
result := convertMemory(memory, "2026-03-30T10:00:00Z")
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 inventory-only DIMM, got %d", len(result))
|
|
}
|
|
if result[0].PartNumber != "HMCG88AEBRA115N" || result[0].SerialNumber != "2B5F92C6" || result[0].SizeMB != 0 {
|
|
t.Fatalf("unexpected converted memory: %+v", result[0])
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_CPUSerialIsNotSynthesizedAndSocketIsDeduped(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "cpu-dedupe.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindCPU,
|
|
Slot: "CPU1",
|
|
Model: "Xeon Platinum",
|
|
Cores: 56,
|
|
Status: "OK",
|
|
Details: map[string]any{
|
|
"socket": 1,
|
|
},
|
|
},
|
|
},
|
|
CPUs: []models.CPU{
|
|
{Socket: 1, Model: "Xeon Platinum", Cores: 56, Status: "OK"},
|
|
{Socket: 2, Model: "Xeon Platinum", Cores: 56, Status: "OK"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.CPUs) != 2 {
|
|
t.Fatalf("expected exactly two CPUs after socket dedupe, got %d", len(out.Hardware.CPUs))
|
|
}
|
|
for _, cpu := range out.Hardware.CPUs {
|
|
if cpu.SerialNumber != "" {
|
|
t.Fatalf("expected CPU serial to stay empty when source serial is absent, got %q", cpu.SerialNumber)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_ExportsEventLogsAndOmitsPCIeBDFJSON(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "events.json",
|
|
CollectedAt: time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC),
|
|
Events: []models.Event{
|
|
{
|
|
ID: "0x0042",
|
|
Timestamp: time.Date(2026, 3, 15, 11, 59, 0, 0, time.UTC),
|
|
Source: "SEL",
|
|
SensorName: "CPU0_C0D0",
|
|
Severity: models.SeverityWarning,
|
|
Description: "Correctable ECC error threshold exceeded",
|
|
RawData: "sel_record_id=42",
|
|
},
|
|
{
|
|
Source: "LOGPile",
|
|
Severity: models.SeverityWarning,
|
|
Description: "internal warning should not leak to event_logs",
|
|
},
|
|
},
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindPCIe,
|
|
Slot: "",
|
|
BDF: "0000:18:00.0",
|
|
DeviceClass: "NetworkController",
|
|
Manufacturer: "Mellanox",
|
|
Model: "ConnectX-6",
|
|
Status: "OK",
|
|
Details: map[string]any{
|
|
"manufactured_year_week": "2024-W07",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.EventLogs) != 1 {
|
|
t.Fatalf("expected 1 exported event log, got %d", len(out.Hardware.EventLogs))
|
|
}
|
|
log := out.Hardware.EventLogs[0]
|
|
if log.Source != "bmc" {
|
|
t.Fatalf("expected SEL source to map to bmc, got %#v", log)
|
|
}
|
|
if log.ComponentRef != "CPU0_C0D0" {
|
|
t.Fatalf("expected sensor name to map to component_ref, got %#v", log)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected 1 pcie device, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
if out.Hardware.PCIeDevices[0].Slot != "0000:18:00.0" {
|
|
t.Fatalf("expected slot to fall back to BDF, got %#v", out.Hardware.PCIeDevices[0])
|
|
}
|
|
if out.Hardware.PCIeDevices[0].ManufacturedYearWeek != "2024-W07" {
|
|
t.Fatalf("expected manufactured_year_week to be exported, got %#v", out.Hardware.PCIeDevices[0])
|
|
}
|
|
|
|
payload, err := json.Marshal(out)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal() failed: %v", err)
|
|
}
|
|
if strings.Contains(string(payload), `"bdf"`) {
|
|
t.Fatalf("expected pcie bdf field to stay out of JSON payload: %s", payload)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_EventLogSourceMappingSupportsDellAndHostSyslog(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "event-source-map.json",
|
|
CollectedAt: time.Date(2026, 3, 15, 12, 0, 0, 0, time.UTC),
|
|
Events: []models.Event{
|
|
{
|
|
ID: "SYS1001",
|
|
Timestamp: time.Date(2026, 3, 15, 11, 58, 0, 0, time.UTC),
|
|
Source: "iDRAC",
|
|
SensorName: "NIC.Slot.1-1-1",
|
|
Severity: models.SeverityWarning,
|
|
Description: "Link is down",
|
|
},
|
|
{
|
|
ID: "syslog_1",
|
|
Timestamp: time.Date(2026, 3, 15, 11, 59, 0, 0, time.UTC),
|
|
Source: "syslog",
|
|
SensorName: "systemd[1]",
|
|
Severity: models.SeverityInfo,
|
|
Description: "Started Example Service",
|
|
},
|
|
},
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.EventLogs) != 2 {
|
|
t.Fatalf("expected 2 event logs, got %d", len(out.Hardware.EventLogs))
|
|
}
|
|
if out.Hardware.EventLogs[0].Source != "bmc" {
|
|
t.Fatalf("expected iDRAC event to map to bmc, got %#v", out.Hardware.EventLogs[0])
|
|
}
|
|
if out.Hardware.EventLogs[1].Source != "host" {
|
|
t.Fatalf("expected syslog event to map to host, got %#v", out.Hardware.EventLogs[1])
|
|
}
|
|
}
|
|
|
|
func TestConvertStorage(t *testing.T) {
|
|
storage := []models.Storage{
|
|
{
|
|
Slot: "OB01",
|
|
Type: "NVMe",
|
|
Model: "INTEL SSDPF2KX076T1",
|
|
SerialNumber: "BTAX41900GF87P6DGN",
|
|
Present: true,
|
|
},
|
|
{
|
|
Slot: "OB02",
|
|
Type: "NVMe",
|
|
Model: "INTEL SSDPF2KX076T1",
|
|
SerialNumber: "",
|
|
Present: true,
|
|
},
|
|
}
|
|
|
|
result := convertStorage(storage, "2026-02-10T15:30:00Z")
|
|
|
|
if len(result) != 2 {
|
|
t.Fatalf("expected both inventory slots to be exported, got %d", len(result))
|
|
}
|
|
|
|
if result[0].Status != "Unknown" {
|
|
t.Errorf("expected Unknown status, got %q", result[0].Status)
|
|
}
|
|
if result[1].SerialNumber != "" {
|
|
t.Errorf("expected empty serial for second storage slot, got %q", result[1].SerialNumber)
|
|
}
|
|
if result[1].Present == nil || !*result[1].Present {
|
|
t.Fatalf("expected present=true to be preserved for populated slot without serial")
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_SkipsAMIVirtualStorageDevices(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "virtual-media.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Storage: []models.Storage{
|
|
{
|
|
Slot: "USB_Device1_Port4",
|
|
Type: "HDD",
|
|
Model: "Virtual Cdrom Device",
|
|
SerialNumber: "AAAABBBBCCCC1",
|
|
Manufacturer: "American Megatrends Inc.",
|
|
Interface: "USB",
|
|
Present: true,
|
|
},
|
|
{
|
|
Slot: "OB01",
|
|
Type: "NVMe",
|
|
Model: "Memblaze PBlaze7",
|
|
SerialNumber: "REAL-NVME-001",
|
|
Manufacturer: "Memblaze",
|
|
Interface: "NVMe",
|
|
Present: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected only one real storage device to remain, got %d", len(out.Hardware.Storage))
|
|
}
|
|
if out.Hardware.Storage[0].SerialNumber != "REAL-NVME-001" {
|
|
t.Fatalf("expected virtual AMI storage to be skipped, got %#v", out.Hardware.Storage)
|
|
}
|
|
}
|
|
|
|
func TestConvertStorage_RemainingEndurance(t *testing.T) {
|
|
pct100 := 100
|
|
pct3 := 3
|
|
storage := []models.Storage{
|
|
{
|
|
Slot: "0",
|
|
Model: "HFS480G3H2X069N",
|
|
SerialNumber: "ESEAN5254I030B26B",
|
|
Present: true,
|
|
RemainingEndurancePct: &pct100,
|
|
},
|
|
{
|
|
Slot: "1",
|
|
Model: "HFS480G3H2X069N",
|
|
SerialNumber: "ESEAN5254I030B26C",
|
|
Present: true,
|
|
// no endurance data
|
|
},
|
|
{
|
|
Slot: "2",
|
|
Model: "HFS480G3H2X069N",
|
|
SerialNumber: "ESEAN5254I030B26D",
|
|
Present: true,
|
|
RemainingEndurancePct: &pct3,
|
|
},
|
|
}
|
|
|
|
result := convertStorage(storage, "2026-03-15T00:00:00Z")
|
|
if len(result) != 3 {
|
|
t.Fatalf("expected 3 results, got %d", len(result))
|
|
}
|
|
if result[0].RemainingEndurancePct == nil || *result[0].RemainingEndurancePct != 100 {
|
|
t.Errorf("slot 0: expected remaining_endurance_pct=100, got %v", result[0].RemainingEndurancePct)
|
|
}
|
|
if result[1].RemainingEndurancePct != nil {
|
|
t.Errorf("slot 1: expected remaining_endurance_pct absent, got %v", *result[1].RemainingEndurancePct)
|
|
}
|
|
if result[2].RemainingEndurancePct == nil || *result[2].RemainingEndurancePct != 3 {
|
|
t.Errorf("slot 2: expected remaining_endurance_pct=3, got %v", result[2].RemainingEndurancePct)
|
|
}
|
|
}
|
|
|
|
func TestConvertPCIeDevices(t *testing.T) {
|
|
hw := &models.HardwareConfig{
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "PCIeCard1",
|
|
VendorID: 32902,
|
|
DeviceID: 2912,
|
|
BDF: "0000:18:00.0",
|
|
DeviceClass: "MassStorageController",
|
|
Manufacturer: "Intel",
|
|
PartNumber: "RSP3DD080F",
|
|
SerialNumber: "RAID-001",
|
|
},
|
|
{
|
|
Slot: "PCIeCard2",
|
|
DeviceClass: "NetworkController",
|
|
Manufacturer: "Mellanox",
|
|
SerialNumber: "", // Should be generated
|
|
},
|
|
},
|
|
GPUs: []models.GPU{
|
|
{
|
|
Slot: "GPU1",
|
|
Model: "NVIDIA A100",
|
|
Manufacturer: "NVIDIA",
|
|
SerialNumber: "GPU-001",
|
|
Status: "OK",
|
|
},
|
|
},
|
|
NetworkAdapters: []models.NetworkAdapter{
|
|
{
|
|
Slot: "NIC1",
|
|
Model: "ConnectX-6",
|
|
Vendor: "Mellanox",
|
|
Present: true,
|
|
SerialNumber: "NIC-001",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
|
|
|
// Should have: 2 PCIe devices + 1 GPU + 1 NIC = 4 total
|
|
if len(result) != 4 {
|
|
t.Fatalf("expected 4 PCIe devices total, got %d", len(result))
|
|
}
|
|
|
|
// Missing serials must remain absent.
|
|
if result[1].SerialNumber != "" {
|
|
t.Errorf("expected empty serial for missing device serial, got %q", result[1].SerialNumber)
|
|
}
|
|
|
|
// Check GPU was included
|
|
foundGPU := false
|
|
for _, dev := range result {
|
|
if dev.SerialNumber == "GPU-001" {
|
|
foundGPU = true
|
|
if dev.DeviceClass != "VideoController" {
|
|
t.Errorf("expected GPU device_class VideoController, got %q", dev.DeviceClass)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !foundGPU {
|
|
t.Error("expected GPU to be included in PCIe devices")
|
|
}
|
|
}
|
|
|
|
func TestConvertPCIeDevices_NVSwitchWithoutSerialRemainsEmpty(t *testing.T) {
|
|
hw := &models.HardwareConfig{
|
|
Firmware: []models.FirmwareInfo{
|
|
{
|
|
DeviceName: "NVSwitch NVSWITCH1 (965-25612-0002-000)",
|
|
Version: "96.10.6D.00.01",
|
|
},
|
|
},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "NVSWITCH1",
|
|
DeviceClass: "NVSwitch",
|
|
BDF: "0000:06:00.0",
|
|
// SerialNumber empty on purpose; should remain empty.
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 PCIe device, got %d", len(result))
|
|
}
|
|
|
|
if result[0].SerialNumber != "" {
|
|
t.Fatalf("expected empty NVSwitch serial, got %q", result[0].SerialNumber)
|
|
}
|
|
if result[0].Firmware != "96.10.6D.00.01" {
|
|
t.Fatalf("expected NVSwitch firmware 96.10.6D.00.01, got %q", result[0].Firmware)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_MapsHGXNVSwitchFirmwareToPCIeDevice(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Firmware: []models.FirmwareInfo{
|
|
{DeviceName: "HGX_FW_ERoT_NVSwitch_0", Version: "00.02.0192.0000_n00"},
|
|
{DeviceName: "HGX_FW_NVSwitch_0", Version: "96.10.73.00.01"},
|
|
},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "NVSwitch_0",
|
|
DeviceClass: "NVSwitch",
|
|
Manufacturer: "NVIDIA",
|
|
Details: map[string]any{
|
|
"temperature_c": 31.59375,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected one NVSwitch PCIe device, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
got := out.Hardware.PCIeDevices[0]
|
|
if got.Firmware != "96.10.73.00.01" {
|
|
t.Fatalf("expected HGX NVSwitch firmware to map to device, got %#v", got)
|
|
}
|
|
if got.TemperatureC != 31.59375 {
|
|
t.Fatalf("expected NVSwitch temperature to be exported, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildNVSwitchFirmwareBySlot_SkipsERoTFirmware(t *testing.T) {
|
|
got := buildNVSwitchFirmwareBySlot([]models.FirmwareInfo{
|
|
{DeviceName: "HGX_FW_ERoT_NVSwitch_0", Version: "00.02.0192.0000_n00"},
|
|
{DeviceName: "HGX_FW_NVSwitch_0", Version: "96.10.73.00.01"},
|
|
})
|
|
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected only main NVSwitch firmware to remain, got %#v", got)
|
|
}
|
|
if got["NVSWITCH_0"] != "96.10.73.00.01" {
|
|
t.Fatalf("expected main NVSwitch firmware, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertPCIeDevices_SkipsDisplayControllerDuplicates(t *testing.T) {
|
|
hw := &models.HardwareConfig{
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "#GPU0",
|
|
DeviceClass: "3D Controller",
|
|
},
|
|
},
|
|
GPUs: []models.GPU{
|
|
{
|
|
Slot: "#GPU0",
|
|
Model: "B200 180GB HBM3e",
|
|
Manufacturer: "NVIDIA",
|
|
SerialNumber: "1655024043371",
|
|
Status: "OK",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected only dedicated GPU record without duplicate display PCIe, got %d", len(result))
|
|
}
|
|
if result[0].DeviceClass != "VideoController" {
|
|
t.Fatalf("expected GPU record with VideoController class, got %q", result[0].DeviceClass)
|
|
}
|
|
if result[0].Status != "OK" {
|
|
t.Fatalf("expected GPU status OK, got %q", result[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestConvertPCIeDevices_MapsGPUStatusHistory(t *testing.T) {
|
|
hw := &models.HardwareConfig{
|
|
GPUs: []models.GPU{
|
|
{
|
|
Slot: "#GPU6",
|
|
Model: "B200 180GB HBM3e",
|
|
Manufacturer: "NVIDIA",
|
|
SerialNumber: "1655024043204",
|
|
Status: "Critical",
|
|
StatusHistory: []models.StatusHistoryEntry{
|
|
{
|
|
Status: "Critical",
|
|
ChangedAt: time.Date(2026, 1, 12, 15, 5, 18, 0, time.UTC),
|
|
Details: "BIOS miss F_GPU6",
|
|
},
|
|
},
|
|
ErrorDescription: "BIOS miss F_GPU6",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPCIeDevices(hw, "2026-02-10T15:30:00Z")
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 converted GPU, got %d", len(result))
|
|
}
|
|
|
|
if len(result[0].StatusHistory) != 1 {
|
|
t.Fatalf("expected 1 history entry, got %d", len(result[0].StatusHistory))
|
|
}
|
|
if result[0].StatusHistory[0].ChangedAt != "2026-01-12T15:05:18Z" {
|
|
t.Fatalf("unexpected history changed_at: %q", result[0].StatusHistory[0].ChangedAt)
|
|
}
|
|
}
|
|
|
|
func TestConvertPowerSupplies(t *testing.T) {
|
|
psus := []models.PSU{
|
|
{
|
|
Slot: "0",
|
|
Present: true,
|
|
Model: "GW-CRPS3000LW",
|
|
Vendor: "Great Wall",
|
|
WattageW: 3000,
|
|
SerialNumber: "PSU-001",
|
|
Status: "OK",
|
|
},
|
|
{
|
|
Slot: "1",
|
|
Present: false,
|
|
SerialNumber: "", // Not present, should be skipped
|
|
},
|
|
}
|
|
|
|
result := convertPowerSupplies(psus, "2026-02-10T15:30:00Z")
|
|
|
|
if len(result) != 1 {
|
|
t.Fatalf("expected 1 PSU (skipped empty), got %d", len(result))
|
|
}
|
|
|
|
if result[0].Status != "OK" {
|
|
t.Errorf("expected OK status, got %q", result[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestConvertBoardNormalizesNULL(t *testing.T) {
|
|
board := convertBoard(models.BoardInfo{
|
|
Manufacturer: " NULL ",
|
|
ProductName: "null",
|
|
SerialNumber: "TEST123",
|
|
})
|
|
|
|
if board.Manufacturer != "" {
|
|
t.Fatalf("expected empty manufacturer, got %q", board.Manufacturer)
|
|
}
|
|
if board.ProductName != "" {
|
|
t.Fatalf("expected empty product_name, got %q", board.ProductName)
|
|
}
|
|
}
|
|
|
|
func TestSourceTypeOmittedWhenInvalidOrEmpty(t *testing.T) {
|
|
result, err := ConvertToReanimator(&models.AnalysisResult{
|
|
Filename: "redfish://10.0.0.1",
|
|
SourceType: "archive",
|
|
TargetHost: "10.0.0.1",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "TEST123"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
payload, err := json.Marshal(result)
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
if !strings.Contains(string(payload), `"source_type":"logfile"`) {
|
|
t.Fatalf("expected archive source_type to map to logfile, got %s", string(payload))
|
|
}
|
|
}
|
|
|
|
func TestTargetHostOmittedWhenUnavailable(t *testing.T) {
|
|
result, err := ConvertToReanimator(&models.AnalysisResult{
|
|
Filename: "test.json",
|
|
SourceType: "api",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "TEST123"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
payload, err := json.Marshal(result)
|
|
if err != nil {
|
|
t.Fatalf("marshal failed: %v", err)
|
|
}
|
|
if strings.Contains(string(payload), `"target_host"`) {
|
|
t.Fatalf("expected target_host to be omitted when unavailable, got %s", string(payload))
|
|
}
|
|
}
|
|
|
|
func TestInferTargetHost(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
targetHost string
|
|
filename string
|
|
want string
|
|
}{
|
|
{
|
|
name: "explicit target host wins",
|
|
targetHost: "10.0.0.10",
|
|
filename: "redfish://10.0.0.20",
|
|
want: "10.0.0.10",
|
|
},
|
|
{
|
|
name: "hostname from URL",
|
|
filename: "redfish://10.10.10.103",
|
|
want: "10.10.10.103",
|
|
},
|
|
{
|
|
name: "ip extracted from archive name",
|
|
filename: "nvidia_bug_report_192.168.12.34.tar.gz",
|
|
want: "192.168.12.34",
|
|
},
|
|
{
|
|
name: "no host available",
|
|
filename: "test.json",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := inferTargetHost(tt.targetHost, tt.filename)
|
|
if got != tt.want {
|
|
t.Fatalf("inferTargetHost() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_DeduplicatesAllSections(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "dup-test.json",
|
|
CollectedAt: time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC),
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Firmware: []models.FirmwareInfo{
|
|
{DeviceName: "BMC", Version: "1.0"},
|
|
{DeviceName: "BMC", Version: "1.1"},
|
|
},
|
|
CPUs: []models.CPU{
|
|
{Socket: 0, Model: "CPU-A"},
|
|
{Socket: 0, Model: "CPU-A-DUP"},
|
|
},
|
|
Memory: []models.MemoryDIMM{
|
|
{Slot: "DIMM_A1", Present: true, SizeMB: 32768, SerialNumber: "MEM-1", Status: "OK"},
|
|
{Slot: "DIMM_A1", Present: true, SizeMB: 32768, SerialNumber: "MEM-1-DUP", Status: "OK"},
|
|
},
|
|
Storage: []models.Storage{
|
|
{Slot: "U.2-1", SerialNumber: "SSD-1", Model: "Disk1", Present: true},
|
|
{Slot: "U.2-2", SerialNumber: "SSD-1", Model: "Disk1-dup", Present: true},
|
|
},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{Slot: "#GPU0", DeviceClass: "3D Controller", BDF: "17:00.0"},
|
|
{Slot: "SLOT-NIC1", DeviceClass: "NetworkController", BDF: "18:00.0"},
|
|
{Slot: "SLOT-NIC1", DeviceClass: "NetworkController", BDF: "18:00.1"},
|
|
},
|
|
GPUs: []models.GPU{
|
|
{Slot: "#GPU0", Model: "B200 180GB HBM3e", SerialNumber: "GPU-1", Status: "OK"},
|
|
},
|
|
PowerSupply: []models.PSU{
|
|
{Slot: "0", Present: true, SerialNumber: "PSU-1", Status: "OK"},
|
|
{Slot: "1", Present: true, SerialNumber: "PSU-1", Status: "OK"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
|
|
if len(out.Hardware.Firmware) != 1 {
|
|
t.Fatalf("expected deduped firmware len=1, got %d", len(out.Hardware.Firmware))
|
|
}
|
|
if len(out.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected cpus len=1 after socket dedupe, got %d", len(out.Hardware.CPUs))
|
|
}
|
|
if len(out.Hardware.Memory) != 1 {
|
|
t.Fatalf("expected memory len=1 after slot dedupe, got %d", len(out.Hardware.Memory))
|
|
}
|
|
if len(out.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected deduped storage len=1, got %d", len(out.Hardware.Storage))
|
|
}
|
|
if len(out.Hardware.PowerSupplies) != 1 {
|
|
t.Fatalf("expected deduped psu len=1, got %d", len(out.Hardware.PowerSupplies))
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 2 {
|
|
t.Fatalf("expected pcie len=2 after final pcie-class dedupe, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
|
|
gpuCount := 0
|
|
for _, dev := range out.Hardware.PCIeDevices {
|
|
if dev.Slot == "#GPU0" {
|
|
gpuCount++
|
|
}
|
|
}
|
|
if gpuCount != 1 {
|
|
t.Fatalf("expected one merged #GPU0 record, got %d", gpuCount)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_StatusFallbackUsesCollectedAt(t *testing.T) {
|
|
collectedAt := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC)
|
|
input := &models.AnalysisResult{
|
|
Filename: "status-fallback.json",
|
|
CollectedAt: collectedAt,
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Storage: []models.Storage{
|
|
{
|
|
Slot: "U.2-1",
|
|
Model: "PM9A3",
|
|
SerialNumber: "SSD-001",
|
|
Present: true,
|
|
Status: "OK",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected 1 storage entry, got %d", len(out.Hardware.Storage))
|
|
}
|
|
|
|
wantTs := collectedAt.UTC().Format(time.RFC3339)
|
|
got := out.Hardware.Storage[0]
|
|
if got.StatusCheckedAt != wantTs {
|
|
t.Fatalf("expected status_checked_at=%q, got %q", wantTs, got.StatusCheckedAt)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_ExportsStorageInventoryWithoutSerial(t *testing.T) {
|
|
collectedAt := time.Date(2026, 4, 1, 9, 0, 0, 0, time.UTC)
|
|
input := &models.AnalysisResult{
|
|
Filename: "nvme-inventory.json",
|
|
CollectedAt: collectedAt,
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Storage: []models.Storage{
|
|
{
|
|
Slot: "OB01",
|
|
Type: "NVMe",
|
|
Model: "PM9A3",
|
|
SerialNumber: "SSD-001",
|
|
Present: true,
|
|
},
|
|
{
|
|
Slot: "OB02",
|
|
Type: "NVMe",
|
|
Model: "PM9A3",
|
|
Present: true,
|
|
},
|
|
{
|
|
Slot: "OB03",
|
|
Type: "NVMe",
|
|
Model: "PM9A3",
|
|
Present: false,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.Storage) != 3 {
|
|
t.Fatalf("expected 3 storage entries including inventory slots without serial, got %d", len(out.Hardware.Storage))
|
|
}
|
|
if out.Hardware.Storage[1].Slot != "OB02" || out.Hardware.Storage[1].SerialNumber != "" {
|
|
t.Fatalf("expected OB02 storage slot without serial to survive export, got %#v", out.Hardware.Storage[1])
|
|
}
|
|
if out.Hardware.Storage[2].Present == nil || *out.Hardware.Storage[2].Present {
|
|
t.Fatalf("expected OB03 to preserve present=false, got %#v", out.Hardware.Storage[2])
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "fw-filter-test.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
CPUs: []models.CPU{
|
|
{Socket: 0, Model: "Intel Xeon Gold"},
|
|
},
|
|
Firmware: []models.FirmwareInfo{
|
|
{DeviceName: "BIOS", Version: "1.0.0"},
|
|
{DeviceName: "BMC", Version: "2.0.0"},
|
|
{DeviceName: "GPU GPUSXM1 (692-2G520-0280-501)", Version: "96.00.D0.00.03"},
|
|
{DeviceName: "NVSwitch NVSWITCH0 (965-25612-0002-000)", Version: "96.10.6D.00.01"},
|
|
{DeviceName: "NIC #CPU1_PCIE9 (MCX512A-ACAT)", Version: "28.38.1900"},
|
|
{DeviceName: "CPU0 Microcode", Version: "0x2b000643"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
|
|
if len(out.Hardware.Firmware) != 2 {
|
|
t.Fatalf("expected only machine-level firmware entries, got %d", len(out.Hardware.Firmware))
|
|
}
|
|
|
|
got := map[string]string{}
|
|
for _, fw := range out.Hardware.Firmware {
|
|
got[fw.DeviceName] = fw.Version
|
|
}
|
|
|
|
if got["BIOS"] != "1.0.0" {
|
|
t.Fatalf("expected BIOS firmware to be kept")
|
|
}
|
|
if got["BMC"] != "2.0.0" {
|
|
t.Fatalf("expected BMC firmware to be kept")
|
|
}
|
|
if _, exists := got["GPU GPUSXM1 (692-2G520-0280-501)"]; exists {
|
|
t.Fatalf("expected GPU firmware to be excluded from hardware.firmware")
|
|
}
|
|
if _, exists := got["NVSwitch NVSWITCH0 (965-25612-0002-000)"]; exists {
|
|
t.Fatalf("expected NVSwitch firmware to be excluded from hardware.firmware")
|
|
}
|
|
if len(out.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected 1 CPU entry, got %d", len(out.Hardware.CPUs))
|
|
}
|
|
if out.Hardware.CPUs[0].Firmware != "0x2b000643" {
|
|
t.Fatalf("expected CPU firmware field to carry microcode, got %q", out.Hardware.CPUs[0].Firmware)
|
|
}
|
|
}
|
|
|
|
// TestConvertToReanimator_FirmwareExcludesDellFQDDEntries verifies that Dell TSR
|
|
// SoftwareIdentity firmware entries whose Description contains a device-bound FQDD
|
|
// (InfiniBand.Slot.*, RAID.SL.*, etc.) are filtered from hardware.firmware.
|
|
//
|
|
// Regression guard: PowerEdge R6625 (8VS2LG4) — "Mellanox Network Adapter" (FQDD
|
|
// InfiniBand.Slot.1-1) and "PERC H755 Front" (FQDD RAID.SL.3-1) leaked into
|
|
// hardware.firmware. (2026-03-15)
|
|
func TestConvertToReanimator_FirmwareExcludesDellFQDDEntries(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "dell-fw-filter-test.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "8VS2LG4"},
|
|
Firmware: []models.FirmwareInfo{
|
|
// system-level — must be kept
|
|
{DeviceName: "BIOS", Version: "1.15.3", Description: "system bios"},
|
|
{DeviceName: "iDRAC", Version: "7.20.80.50", Description: "idrac card"},
|
|
{DeviceName: "Lifecycle Controller", Version: "7.20.80.50", Description: "idrac lifecycle"},
|
|
// device-bound via FQDD — must be filtered
|
|
{DeviceName: "Mellanox Network Adapter", Version: "20.39.35.60", Description: "InfiniBand.Slot.1-1"},
|
|
{DeviceName: "PERC H755 Front", Version: "52.30.0-6115", Description: "RAID.SL.3-1"},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
|
|
got := make(map[string]string, len(out.Hardware.Firmware))
|
|
for _, fw := range out.Hardware.Firmware {
|
|
got[fw.DeviceName] = fw.Version
|
|
}
|
|
|
|
for _, keep := range []string{"BIOS", "iDRAC", "Lifecycle Controller"} {
|
|
if _, ok := got[keep]; !ok {
|
|
t.Errorf("expected %q in hardware.firmware, but it was missing", keep)
|
|
}
|
|
}
|
|
for _, drop := range []string{"Mellanox Network Adapter", "PERC H755 Front"} {
|
|
if _, ok := got[drop]; ok {
|
|
t.Errorf("%q must not appear in hardware.firmware (device-bound FQDD)", drop)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_UsesCanonicalDevices(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "canonical.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindCPU,
|
|
Slot: "CPU0",
|
|
Model: "INTEL(R) XEON(R)",
|
|
Cores: 32,
|
|
Threads: 64,
|
|
FrequencyMHz: 2100,
|
|
},
|
|
{
|
|
Kind: models.DeviceKindStorage,
|
|
Slot: "U.2-1",
|
|
Model: "Disk1",
|
|
SerialNumber: "SSD-1",
|
|
Present: boolPtr(true),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected cpu from hardware.devices, got %d", len(out.Hardware.CPUs))
|
|
}
|
|
if len(out.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected storage from hardware.devices, got %d", len(out.Hardware.Storage))
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_MergesCanonicalAndLegacyDevices(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "merged.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindPCIe,
|
|
Slot: "PCIe 3",
|
|
Model: "RAID Controller",
|
|
DeviceClass: "raid_controller",
|
|
Status: "ok",
|
|
},
|
|
},
|
|
CPUs: []models.CPU{
|
|
{Socket: 0, Model: "Xeon Platinum", SerialNumber: "CPU-001"},
|
|
},
|
|
Memory: []models.MemoryDIMM{
|
|
{Slot: "DIMM0", Location: "DIMM0", Present: true, SizeMB: 32768, Type: "DDR5", SerialNumber: "MEM-001"},
|
|
},
|
|
Storage: []models.Storage{
|
|
{Slot: "U.2-1", Type: "NVMe", Model: "Drive1", SerialNumber: "SSD-001", Present: true},
|
|
},
|
|
PowerSupply: []models.PSU{
|
|
{Slot: "PSU0", Model: "PSU", SerialNumber: "PSU-001", Present: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected cpu from legacy inventory to survive canonical merge, got %d", len(out.Hardware.CPUs))
|
|
}
|
|
if len(out.Hardware.Memory) != 1 {
|
|
t.Fatalf("expected memory from legacy inventory to survive canonical merge, got %d", len(out.Hardware.Memory))
|
|
}
|
|
if len(out.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected storage from legacy inventory to survive canonical merge, got %d", len(out.Hardware.Storage))
|
|
}
|
|
if len(out.Hardware.PowerSupplies) != 1 {
|
|
t.Fatalf("expected psu from legacy inventory to survive canonical merge, got %d", len(out.Hardware.PowerSupplies))
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected supplemental canonical pcie device to remain present, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_DoesNotMergeStorageIntoPCIeBySharedSerial(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "nvme-redfish.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Storage: []models.Storage{
|
|
{
|
|
Slot: "Disk.Bay.0",
|
|
Type: "NVMe",
|
|
Model: "MZQL21T9HCJR-00A07",
|
|
SerialNumber: "S64GNNFX612200",
|
|
Manufacturer: "Samsung",
|
|
Firmware: "GDC5A02Q",
|
|
Present: true,
|
|
},
|
|
},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "NVMeSSD1",
|
|
BDF: "0000:81:00.0",
|
|
DeviceClass: "MassStorageController",
|
|
Description: "MZQL21T9HCJR-00A07",
|
|
SerialNumber: "S64GNNFX612200",
|
|
Manufacturer: "Samsung",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected storage record to survive shared-serial canonical merge, got %d", len(out.Hardware.Storage))
|
|
}
|
|
if out.Hardware.Storage[0].Slot != "Disk.Bay.0" {
|
|
t.Fatalf("expected storage slot Disk.Bay.0, got %q", out.Hardware.Storage[0].Slot)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 0 {
|
|
t.Fatalf("expected NVMe storage endpoint to be excluded from pcie export, got %d records", len(out.Hardware.PCIeDevices))
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_LeavesStorageControllersInPCIe(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "PCIe Slot 3",
|
|
BDF: "0000:5e:00.0",
|
|
DeviceClass: "MassStorageController",
|
|
Description: "MegaRAID Controller",
|
|
PartNumber: "PERC H755",
|
|
SerialNumber: "RAID-001",
|
|
Manufacturer: "Dell",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected RAID controller to remain in pcie export, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_PCIePlaceholderModelFallsBackToPCIIDs(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindNetwork,
|
|
Slot: "NIC1",
|
|
Model: "Network Device View",
|
|
VendorID: 0x15b3,
|
|
DeviceID: 0x101d,
|
|
Manufacturer: "Mellanox",
|
|
Present: boolPtr(true),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected one pcie export, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
if strings.EqualFold(out.Hardware.PCIeDevices[0].Model, "Network Device View") {
|
|
t.Fatalf("expected placeholder model to be replaced, got %q", out.Hardware.PCIeDevices[0].Model)
|
|
}
|
|
if out.Hardware.PCIeDevices[0].SerialNumber != "" {
|
|
t.Fatalf("expected missing pcie serial to stay empty, got %q", out.Hardware.PCIeDevices[0].SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_SkipsPlaceholderNetworkPCIeRecords(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindNetwork,
|
|
Slot: "1",
|
|
Status: "Unknown",
|
|
},
|
|
{
|
|
Kind: models.DeviceKindNetwork,
|
|
Slot: "NIC2",
|
|
Model: "ConnectX-7",
|
|
Manufacturer: "NVIDIA",
|
|
Present: boolPtr(true),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected only one meaningful pcie-class device, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
if out.Hardware.PCIeDevices[0].Slot != "NIC2" {
|
|
t.Fatalf("expected placeholder numeric-slot NIC to be skipped, got %+v", out.Hardware.PCIeDevices)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_ExportsSensorsAndPSUTelemetry(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "vitals.json",
|
|
Sensors: []models.SensorReading{
|
|
{Name: "FAN1", Type: "fan", Value: 4200, Unit: "RPM", Status: "OK"},
|
|
{Name: "12V Rail", Type: "voltage", Value: 12.1, Unit: "V", Status: "OK"},
|
|
{Name: "CPU0 Temp", Type: "temperature", Value: 71, Unit: "C", Status: "Warning"},
|
|
{Name: "Humidity", Type: "humidity", Value: 38.5, Unit: "%", Status: "OK"},
|
|
},
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindGPU,
|
|
Slot: "#GPU0",
|
|
Model: "B200 180GB HBM3e",
|
|
SerialNumber: "GPU-001",
|
|
BDF: "0000:17:00.0",
|
|
},
|
|
{
|
|
Kind: models.DeviceKindPSU,
|
|
Slot: "PSU0",
|
|
SerialNumber: "PSU-001",
|
|
Present: boolPtr(true),
|
|
InputPowerW: 1400,
|
|
OutputPowerW: 1300,
|
|
InputVoltage: 229.5,
|
|
TemperatureC: 44,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected one pcie device, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
pcie := out.Hardware.PCIeDevices[0]
|
|
if pcie.TemperatureC != 0 {
|
|
t.Fatalf("expected canonical GPU telemetry to stay off the component unless sourced from details/gpu path, got %.2f", pcie.TemperatureC)
|
|
}
|
|
|
|
if len(out.Hardware.PowerSupplies) != 1 {
|
|
t.Fatalf("expected one PSU, got %d", len(out.Hardware.PowerSupplies))
|
|
}
|
|
psu := out.Hardware.PowerSupplies[0]
|
|
if psu.InputPowerW != 1400 {
|
|
t.Fatalf("expected PSU input power 1400W, got %.2f", psu.InputPowerW)
|
|
}
|
|
if psu.TemperatureC != 44 {
|
|
t.Fatalf("expected PSU temperature 44C, got %.2f", psu.TemperatureC)
|
|
}
|
|
if out.Hardware.Sensors == nil {
|
|
t.Fatalf("expected sensors section")
|
|
}
|
|
if len(out.Hardware.Sensors.Fans) != 1 || out.Hardware.Sensors.Fans[0].RPM != 4200 {
|
|
t.Fatalf("expected fan sensor export, got %#v", out.Hardware.Sensors.Fans)
|
|
}
|
|
if len(out.Hardware.Sensors.Power) != 1 || out.Hardware.Sensors.Power[0].VoltageV != 12.1 {
|
|
t.Fatalf("expected power sensor export, got %#v", out.Hardware.Sensors.Power)
|
|
}
|
|
if len(out.Hardware.Sensors.Temperatures) != 1 || out.Hardware.Sensors.Temperatures[0].Celsius != 71 {
|
|
t.Fatalf("expected temperature sensor export, got %#v", out.Hardware.Sensors.Temperatures)
|
|
}
|
|
if len(out.Hardware.Sensors.Other) != 1 || out.Hardware.Sensors.Other[0].Unit != "%" {
|
|
t.Fatalf("expected other sensor export, got %#v", out.Hardware.Sensors.Other)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_SkipsSensorsWithoutNumericReadings(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "sensor-gaps.json",
|
|
Sensors: []models.SensorReading{
|
|
{Name: "CPU0 Temp", Type: "temperature", Status: "OK", RawValue: "N/A"},
|
|
{Name: "PSU1 Power", Type: "power", Status: "OK", RawValue: ""},
|
|
{Name: "Fan1", Type: "fan", Status: "OK", RawValue: "not present"},
|
|
{Name: "Humidity", Type: "humidity", Status: "OK", RawValue: "unknown"},
|
|
},
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if out.Hardware.Sensors != nil {
|
|
t.Fatalf("expected sensors to be omitted when all readings are non-numeric, got %+v", out.Hardware.Sensors)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_MergesSiblingPowerSensors(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "power-sensors.json",
|
|
Sensors: []models.SensorReading{
|
|
{Name: "Power Supply Bay 8_InputPower", Type: "power", Value: 231, Unit: "W", Status: "OK"},
|
|
{Name: "Power Supply Bay 8_InputVoltage", Type: "voltage", Value: 228, Unit: "V", Status: "OK"},
|
|
},
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if out.Hardware.Sensors == nil || len(out.Hardware.Sensors.Power) != 1 {
|
|
t.Fatalf("expected one merged power sensor, got %#v", out.Hardware.Sensors)
|
|
}
|
|
got := out.Hardware.Sensors.Power[0]
|
|
if got.Name != "Power Supply Bay 8" {
|
|
t.Fatalf("expected merged sensor name, got %q", got.Name)
|
|
}
|
|
if got.PowerW != 231 || got.VoltageV != 228 {
|
|
t.Fatalf("expected merged power/voltage readings, got %#v", got)
|
|
}
|
|
if got.Location != "" {
|
|
t.Fatalf("expected sensor location to be omitted, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_MergesInspurPSUInputAndOutputSensors(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
|
},
|
|
Sensors: []models.SensorReading{
|
|
{Name: "PSU0_VIN", Type: "voltage", Value: 224, Unit: "V", Status: "OK"},
|
|
{Name: "PSU0_PIN", Type: "power", Value: 120, Unit: "W", Status: "OK"},
|
|
{Name: "PSU0_VOUT", Type: "voltage", Value: 12, Unit: "V", Status: "OK"},
|
|
{Name: "PSU0_POUT", Type: "power", Value: 88, Unit: "W", Status: "OK"},
|
|
{Name: "PSU0_OutputPower", Type: "power", Value: 95, Unit: "W", Status: "OK"},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if out.Hardware.Sensors == nil || len(out.Hardware.Sensors.Power) != 2 {
|
|
t.Fatalf("expected 2 grouped PSU power sensors, got %#v", out.Hardware.Sensors)
|
|
}
|
|
|
|
byName := map[string]ReanimatorPowerSensor{}
|
|
for _, item := range out.Hardware.Sensors.Power {
|
|
byName[item.Name] = item
|
|
}
|
|
if got, ok := byName["PSU0"]; !ok || got.VoltageV != 224 || got.PowerW != 120 {
|
|
t.Fatalf("expected PSU0 input group with VIN/PIN merged, got %#v", byName)
|
|
}
|
|
if got, ok := byName["PSU0_Output"]; !ok || got.VoltageV != 12 || got.PowerW != 95 {
|
|
t.Fatalf("expected PSU0 output group with VOUT/POUT merged, got %#v", byName)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_PreservesCanonicalDedupWithoutDeviceVitals(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "dedup-vitals.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "#GPU0",
|
|
BDF: "0000:17:00.0",
|
|
DeviceClass: "3D Controller",
|
|
PartNumber: "Generic Display",
|
|
Manufacturer: "NVIDIA",
|
|
SerialNumber: "GPU-SN-001",
|
|
},
|
|
},
|
|
GPUs: []models.GPU{
|
|
{
|
|
Slot: "#GPU0",
|
|
BDF: "0000:17:00.0",
|
|
Model: "B200 180GB HBM3e",
|
|
Manufacturer: "NVIDIA",
|
|
SerialNumber: "GPU-SN-001",
|
|
Temperature: 67,
|
|
Power: 330,
|
|
Status: "OK",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected deduped one pcie entry, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
got := out.Hardware.PCIeDevices[0]
|
|
if got.DeviceClass != "VideoController" {
|
|
t.Fatalf("expected GPU to export as VideoController, got %q", got.DeviceClass)
|
|
}
|
|
if got.TemperatureC != 67 {
|
|
t.Fatalf("expected deduped GPU temperature 67C, got %.2f", got.TemperatureC)
|
|
}
|
|
if got.PowerW != 330 {
|
|
t.Fatalf("expected deduped GPU power 330W, got %.2f", got.PowerW)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_DedupesLooseCanonicalNICAndPCIeEntries(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "loose-dedup.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "Slot 4",
|
|
DeviceClass: "NetworkController",
|
|
VendorID: 0x15b3,
|
|
DeviceID: 0x1021,
|
|
Manufacturer: "Mellanox",
|
|
PartNumber: "MCX623106AC-CDAT",
|
|
},
|
|
},
|
|
NetworkAdapters: []models.NetworkAdapter{
|
|
{
|
|
Slot: "Slot 4",
|
|
Model: "ConnectX-6",
|
|
VendorID: 0x15b3,
|
|
DeviceID: 0x1021,
|
|
Vendor: "Mellanox",
|
|
MACAddresses: []string{"00:11:22:33:44:55"},
|
|
PortCount: 2,
|
|
Present: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected one merged loose-key pcie entry, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
got := out.Hardware.PCIeDevices[0]
|
|
if got.Model == "" {
|
|
t.Fatalf("expected merged pcie entry to retain a model, got empty")
|
|
}
|
|
if len(got.MACAddresses) != 1 || got.MACAddresses[0] != "00:11:22:33:44:55" {
|
|
t.Fatalf("expected MACs from NIC side after loose merge, got %#v", got.MACAddresses)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_ExportsContractV24Telemetry(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "contract-v24.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindCPU,
|
|
Slot: "CPU0",
|
|
Model: "INTEL(R) XEON(R) GOLD 6530",
|
|
Details: map[string]any{
|
|
"socket": 0,
|
|
"temperature_c": 61.5,
|
|
"power_w": 182.0,
|
|
"throttled": false,
|
|
"correctable_error_count": int64(4),
|
|
"uncorrectable_error_count": int64(1),
|
|
"life_remaining_pct": 98.5,
|
|
"life_used_pct": 1.5,
|
|
},
|
|
},
|
|
{
|
|
Kind: models.DeviceKindMemory,
|
|
Slot: "DIMM_A1",
|
|
SerialNumber: "MEM-001",
|
|
Present: boolPtr(true),
|
|
SizeMB: 32768,
|
|
Type: "DDR5",
|
|
Details: map[string]any{
|
|
"temperature_c": 43.0,
|
|
"correctable_ecc_error_count": int64(2),
|
|
"uncorrectable_ecc_error_count": int64(0),
|
|
"life_remaining_pct": 99.0,
|
|
"spare_blocks_remaining_pct": 97.0,
|
|
"performance_degraded": false,
|
|
},
|
|
},
|
|
{
|
|
Kind: models.DeviceKindStorage,
|
|
Slot: "U.2-1",
|
|
SerialNumber: "SSD-001",
|
|
Model: "PM9A3",
|
|
Present: boolPtr(true),
|
|
Details: map[string]any{
|
|
"temperature_c": 38.5,
|
|
"power_on_hours": int64(12450),
|
|
"unsafe_shutdowns": int64(3),
|
|
"written_bytes": int64(9876543210),
|
|
"life_remaining_pct": 91.0,
|
|
"available_spare_pct": 88.0,
|
|
"offline_uncorrectable": int64(0),
|
|
},
|
|
},
|
|
{
|
|
Kind: models.DeviceKindPCIe,
|
|
Slot: "PCIeCard2",
|
|
SerialNumber: "NIC-001",
|
|
DeviceClass: "EthernetController",
|
|
NUMANode: 1,
|
|
Details: map[string]any{
|
|
"temperature_c": 48.5,
|
|
"power_w": 18.2,
|
|
"life_remaining_pct": 95.0,
|
|
"ecc_corrected_total": int64(12),
|
|
"battery_health_pct": 87.0,
|
|
"sfp_temperature_c": 36.2,
|
|
"sfp_tx_power_dbm": -1.8,
|
|
"sfp_rx_power_dbm": -2.1,
|
|
"sfp_bias_ma": 5.5,
|
|
},
|
|
},
|
|
{
|
|
Kind: models.DeviceKindPSU,
|
|
Slot: "PSU0",
|
|
SerialNumber: "PSU-001",
|
|
Present: boolPtr(true),
|
|
Details: map[string]any{
|
|
"life_remaining_pct": 97.0,
|
|
"life_used_pct": 3.0,
|
|
},
|
|
TemperatureC: 39,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
|
|
if got := out.Hardware.CPUs[0]; got.TemperatureC != 61.5 || got.PowerW != 182.0 || got.Throttled == nil || *got.Throttled {
|
|
t.Fatalf("unexpected CPU telemetry: %#v", got)
|
|
}
|
|
if got := out.Hardware.Memory[0]; got.TemperatureC != 43.0 || got.CorrectableECCErrorCount != 2 || got.PerformanceDegraded == nil || *got.PerformanceDegraded {
|
|
t.Fatalf("unexpected memory telemetry: %#v", got)
|
|
}
|
|
if got := out.Hardware.Storage[0]; got.TemperatureC != 38.5 || got.PowerOnHours != 12450 || got.LifeRemainingPct != 91.0 {
|
|
t.Fatalf("unexpected storage telemetry: %#v", got)
|
|
}
|
|
if got := out.Hardware.PCIeDevices[0]; got.NUMANode != 1 || got.TemperatureC != 48.5 || got.PowerW != 18.2 || got.SFPTemperatureC != 36.2 {
|
|
t.Fatalf("unexpected PCIe telemetry: %#v", got)
|
|
}
|
|
if got := out.Hardware.PowerSupplies[0]; got.TemperatureC != 39 || got.LifeRemainingPct != 97.0 || got.LifeUsedPct != 3.0 {
|
|
t.Fatalf("unexpected PSU telemetry: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_UnifiesEthernetAndNetworkControllers(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-123"},
|
|
Devices: []models.HardwareDevice{
|
|
{
|
|
Kind: models.DeviceKindPCIe,
|
|
Slot: "PCIe1",
|
|
DeviceClass: "EthernetController",
|
|
Present: boolPtr(true),
|
|
SerialNumber: "ETH-001",
|
|
},
|
|
{
|
|
Kind: models.DeviceKindNetwork,
|
|
Slot: "NIC1",
|
|
Model: "Ethernet Adapter",
|
|
Present: boolPtr(true),
|
|
SerialNumber: "NIC-001",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 2 {
|
|
t.Fatalf("expected two pcie-class exports, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
for _, dev := range out.Hardware.PCIeDevices {
|
|
if dev.DeviceClass != "NetworkController" {
|
|
t.Fatalf("expected unified NetworkController class, got %+v", dev)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_PreservesLegacyStorageAndPSUDetails(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "legacy-details.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
Storage: []models.Storage{
|
|
{
|
|
Slot: "Drive0",
|
|
Type: "NVMe",
|
|
Model: "NVMe SSD",
|
|
SerialNumber: "SSD-001",
|
|
Present: true,
|
|
Details: map[string]any{
|
|
"temperature_c": 38.5,
|
|
"power_on_hours": int64(12450),
|
|
"life_remaining_pct": 91.0,
|
|
},
|
|
},
|
|
},
|
|
PowerSupply: []models.PSU{
|
|
{
|
|
Slot: "PSU0",
|
|
Model: "PSU",
|
|
SerialNumber: "PSU-001",
|
|
Present: true,
|
|
Details: map[string]any{
|
|
"temperature_c": 41.0,
|
|
"life_remaining_pct": 96.0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if got := out.Hardware.Storage[0]; got.TemperatureC != 38.5 || got.PowerOnHours != 12450 || got.LifeRemainingPct != 91.0 {
|
|
t.Fatalf("expected storage details from legacy model to survive canonical conversion, got %+v", got)
|
|
}
|
|
if got := out.Hardware.PowerSupplies[0]; got.TemperatureC != 41.0 || got.LifeRemainingPct != 96.0 {
|
|
t.Fatalf("expected psu details from legacy model to survive canonical conversion, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestConvertToReanimator_PreservesLegacyPCIeAndNICDetails(t *testing.T) {
|
|
input := &models.AnalysisResult{
|
|
Filename: "legacy-pcie-details.json",
|
|
Hardware: &models.HardwareConfig{
|
|
BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"},
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "PCIe 1",
|
|
BDF: "0000:17:00.0",
|
|
VendorID: 0x10de,
|
|
DeviceID: 0x2331,
|
|
PartNumber: "H100",
|
|
Manufacturer: "NVIDIA",
|
|
SerialNumber: "GPU-001",
|
|
Details: map[string]any{
|
|
"temperature_c": 48.5,
|
|
"power_w": 315.0,
|
|
"ecc_corrected_total": int64(12),
|
|
"battery_health_pct": 87.0,
|
|
},
|
|
},
|
|
},
|
|
NetworkAdapters: []models.NetworkAdapter{
|
|
{
|
|
Slot: "Slot 4",
|
|
BDF: "0000:18:00.0",
|
|
VendorID: 0x15b3,
|
|
DeviceID: 0x1021,
|
|
Model: "ConnectX-6",
|
|
SerialNumber: "NIC-001",
|
|
Present: true,
|
|
Details: map[string]any{
|
|
"sfp_temperature_c": 34.0,
|
|
"sfp_tx_power_dbm": -1.8,
|
|
"sfp_rx_power_dbm": -2.1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
out, err := ConvertToReanimator(input)
|
|
if err != nil {
|
|
t.Fatalf("ConvertToReanimator() failed: %v", err)
|
|
}
|
|
if len(out.Hardware.PCIeDevices) != 2 {
|
|
t.Fatalf("expected two pcie-class devices, got %d", len(out.Hardware.PCIeDevices))
|
|
}
|
|
foundGPU := false
|
|
foundNIC := false
|
|
for _, dev := range out.Hardware.PCIeDevices {
|
|
switch dev.SerialNumber {
|
|
case "GPU-001":
|
|
foundGPU = true
|
|
if dev.TemperatureC != 48.5 || dev.PowerW != 315.0 || dev.ECCCorrectedTotal != 12 || dev.BatteryHealthPct != 87.0 {
|
|
t.Fatalf("expected GPU telemetry preserved, got %+v", dev)
|
|
}
|
|
case "NIC-001":
|
|
foundNIC = true
|
|
if dev.SFPTemperatureC != 34.0 || dev.SFPTXPowerDBm != -1.8 || dev.SFPRXPowerDBm != -2.1 {
|
|
t.Fatalf("expected NIC sfp telemetry preserved, got %+v", dev)
|
|
}
|
|
}
|
|
}
|
|
if !foundGPU || !foundNIC {
|
|
t.Fatalf("expected both gpu and nic pcie-class exports, got %+v", out.Hardware.PCIeDevices)
|
|
}
|
|
}
|
|
|
|
func boolPtr(v bool) *bool { return &v }
|
|
|
|
// TestIsDeviceBoundFirmwareName verifies that device-bound firmware entries from
|
|
// Supermicro Redfish FirmwareInventory are correctly identified and excluded from
|
|
// hardware.firmware.
|
|
//
|
|
// Regression guard: names like "GPU1 System Slot0" and "NIC1 System Slot0 ..." were
|
|
// not caught because the old check required "gpu " / "nic " (with space), while
|
|
// Supermicro places a digit immediately after the type prefix. (2026-03-12)
|
|
func TestIsDeviceBoundFirmwareName(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
want bool
|
|
}{
|
|
// Supermicro Redfish — device-bound, must be excluded
|
|
{"GPU1 System Slot0", true},
|
|
{"GPU8 System Slot0", true},
|
|
{"NIC1 System Slot0 AOM-DP805-IO", true},
|
|
{"NIC9 System Slot8 MCX75310AAS-NEAT", true},
|
|
{"NVMeController1", true},
|
|
{"Power supply 1", true},
|
|
{"Power supply 6", true},
|
|
{"Software Inventory", true},
|
|
{"software inventory", true}, // case-insensitive
|
|
// Generic / legacy names already covered before this fix
|
|
{"GPU SomeDevice", true},
|
|
{"NIC OnboardLAN", true},
|
|
{"PSU1", true},
|
|
{"NVMe Drive", true},
|
|
// HGX FW ID patterns (in case Id is used as name)
|
|
{"HGX_FW_GPU_SXM_1", true},
|
|
{"HGX_FW_ERoT_NVSwitch_0", true},
|
|
{"HGX_InfoROM_GPU_SXM_2", true},
|
|
// System-level firmware — must NOT be excluded
|
|
{"BIOS", false},
|
|
{"BMC", false},
|
|
{"BMC Backup", false},
|
|
{"Capsule BIOS", false},
|
|
{"Capsule ME", false},
|
|
{"CPLD Motherboard Golden", false},
|
|
{"CPLD AOMboard", false},
|
|
{"FrontFanboard CPLD", false},
|
|
{"Motherboard PCIeSwitch 1", false}, // board-integrated, no device record
|
|
{"SecureBoot", false},
|
|
{"BIOS ME", false},
|
|
}
|
|
for _, tc := range cases {
|
|
got := isDeviceBoundFirmwareName(tc.name)
|
|
if got != tc.want {
|
|
t.Errorf("isDeviceBoundFirmwareName(%q) = %v, want %v", tc.name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestIsDeviceBoundFirmwareFQDD verifies that Dell TSR SoftwareIdentity FQDD strings
|
|
// (stored in FirmwareInfo.Description) correctly identify device-bound entries.
|
|
//
|
|
// Regression guard: "InfiniBand.Slot.1-1" (Mellanox ConnectX-6) and "RAID.SL.3-1"
|
|
// (PERC H755 Front) were not filtered because only "raid.backplane." was listed and
|
|
// "infiniband." was absent. Both firmware entries leaked into hardware.firmware on
|
|
// PowerEdge R6625 (8VS2LG4). (2026-03-15)
|
|
func TestIsDeviceBoundFirmwareFQDD(t *testing.T) {
|
|
cases := []struct {
|
|
desc string
|
|
want bool
|
|
}{
|
|
// Dell TSR SoftwareIdentity FQDDs — device-bound, must be excluded
|
|
{"InfiniBand.Slot.1-1", true}, // Mellanox ConnectX-6
|
|
{"InfiniBand.Slot.2-1", true}, // any InfiniBand slot
|
|
{"RAID.SL.3-1", true}, // PERC H755 Front
|
|
{"RAID.Integrated.1-1", true}, // embedded RAID controller
|
|
{"RAID.Backplane.Firmware.0", true}, // backplane (previously covered)
|
|
{"NIC.Integrated.1-1-1", true}, // embedded NIC
|
|
{"NIC.Slot.1-1-1", true}, // slotted NIC
|
|
{"PSU.Slot.1", true}, // PSU
|
|
{"Disk.Bay.0:Enclosure.Internal.0-1:RAID.SL.3-1", true},
|
|
{"GPU.Slot.1-1", true},
|
|
{"FC.Slot.1-1", true}, // Fibre Channel HBA
|
|
// System-level descriptions — must NOT be excluded
|
|
{"system bios", false},
|
|
{"idrac lifecycle", false},
|
|
{"idrac card", false},
|
|
{"storage controller", false}, // legacy description before fqdd fix
|
|
{"", false},
|
|
}
|
|
for _, tc := range cases {
|
|
got := isDeviceBoundFirmwareFQDD(tc.desc)
|
|
if got != tc.want {
|
|
t.Errorf("isDeviceBoundFirmwareFQDD(%q) = %v, want %v", tc.desc, got, tc.want)
|
|
}
|
|
}
|
|
}
|