509 lines
11 KiB
Go
509 lines
11 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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
// 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{
|
|
PCIeDevices: []models.PCIeDevice{
|
|
{
|
|
Slot: "NVSWITCH1",
|
|
DeviceClass: "NVSwitch",
|
|
BDF: "0000:06:00.0",
|
|
// SerialNumber empty on purpose; should remain empty.
|
|
},
|
|
},
|
|
}
|
|
|
|
result := convertPCIeDevices(hw)
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|