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) } } 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) != 2 { t.Fatalf("expected 2 memory modules, got %d", len(result)) } if result[0].Status != "OK" { t.Errorf("expected OK status for first module, got %q", result[0].Status) } if result[1].Status != "Empty" { t.Errorf("expected Empty status for second module, got %q", result[1].Status) } } 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: "", // No serial - should be skipped Present: true, }, } result := convertStorage(storage, "2026-02-10T15:30:00Z") if len(result) != 1 { t.Fatalf("expected 1 storage device (skipped one without serial), got %d", len(result)) } if result[0].Status != "Unknown" { t.Errorf("expected Unknown status, got %q", result[0].Status) } } 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)) } // Check that serial is empty for second PCIe device (no auto-generation) 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 != "DisplayController" { t.Errorf("expected GPU device_class DisplayController, 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 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 != "DisplayController" { t.Fatalf("expected GPU record with DisplayController 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) } if result[0].StatusAtCollect == nil || result[0].StatusAtCollect.At != "2026-02-10T15:30:00Z" { t.Fatalf("expected status_at_collection to be populated from collected_at") } } 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"`) { t.Fatalf("expected source_type to be omitted for invalid value, 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) != 2 { t.Fatalf("expected cpus len=2 (no serial/bdf dedupe), got %d", len(out.Hardware.CPUs)) } if len(out.Hardware.Memory) != 2 { t.Fatalf("expected memory len=2 (different serials), 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) != 4 { t.Fatalf("expected pcie len=4 with serial->bdf dedupe, got %d", len(out.Hardware.PCIeDevices)) } gpuCount := 0 for _, dev := range out.Hardware.PCIeDevices { if dev.Slot == "#GPU0" { gpuCount++ } } if gpuCount != 2 { t.Fatalf("expected two #GPU0 records (pcie+gpu kinds), got %d", gpuCount) } } func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) { input := &models.AnalysisResult{ Filename: "fw-filter-test.json", Hardware: &models.HardwareConfig{ BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"}, 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") } } 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 boolPtr(v bool) *bool { return &v }