394 lines
9.4 KiB
Go
394 lines
9.4 KiB
Go
package unraid
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
|
)
|
|
|
|
func TestDetect(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
files []parser.ExtractedFile
|
|
wantMin int
|
|
wantMax int
|
|
shouldFind bool
|
|
}{
|
|
{
|
|
name: "typical unraid diagnostics",
|
|
files: []parser.ExtractedFile{
|
|
{
|
|
Path: "box3-diagnostics-20260205-2333/unraid-7.2.0.txt",
|
|
Content: []byte("7.2.0\n"),
|
|
},
|
|
{
|
|
Path: "box3-diagnostics-20260205-2333/system/vars.txt",
|
|
Content: []byte("[parity] => Array\n[disk1] => Array\n"),
|
|
},
|
|
},
|
|
wantMin: 50,
|
|
wantMax: 100,
|
|
shouldFind: true,
|
|
},
|
|
{
|
|
name: "unraid with kernel marker",
|
|
files: []parser.ExtractedFile{
|
|
{
|
|
Path: "diagnostics/system/lscpu.txt",
|
|
Content: []byte("Unraid kernel build 6.12.54"),
|
|
},
|
|
},
|
|
wantMin: 50,
|
|
wantMax: 100,
|
|
shouldFind: true,
|
|
},
|
|
{
|
|
name: "not unraid",
|
|
files: []parser.ExtractedFile{
|
|
{
|
|
Path: "some/random/file.txt",
|
|
Content: []byte("just some random content"),
|
|
},
|
|
},
|
|
wantMin: 0,
|
|
wantMax: 0,
|
|
shouldFind: false,
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := p.Detect(tt.files)
|
|
|
|
if tt.shouldFind && got < tt.wantMin {
|
|
t.Errorf("Detect() = %v, want at least %v", got, tt.wantMin)
|
|
}
|
|
|
|
if got > tt.wantMax {
|
|
t.Errorf("Detect() = %v, want at most %v", got, tt.wantMax)
|
|
}
|
|
|
|
if !tt.shouldFind && got > 0 {
|
|
t.Errorf("Detect() = %v, want 0 (should not detect)", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParse_Version(t *testing.T) {
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "unraid-7.2.0.txt",
|
|
Content: []byte("7.2.0\n"),
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
result, err := p.Parse(files)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if len(result.Hardware.Firmware) == 0 {
|
|
t.Fatal("expected firmware info")
|
|
}
|
|
|
|
fw := result.Hardware.Firmware[0]
|
|
if fw.DeviceName != "Unraid OS" {
|
|
t.Errorf("DeviceName = %v, want 'Unraid OS'", fw.DeviceName)
|
|
}
|
|
|
|
if fw.Version != "7.2.0" {
|
|
t.Errorf("Version = %v, want '7.2.0'", fw.Version)
|
|
}
|
|
}
|
|
|
|
func TestParse_CPU(t *testing.T) {
|
|
lscpuContent := `Architecture: x86_64
|
|
CPU op-mode(s): 32-bit, 64-bit
|
|
CPU(s): 16
|
|
Model name: Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz
|
|
Core(s) per socket: 8
|
|
Socket(s): 1
|
|
CPU max MHz: 3400.0000
|
|
`
|
|
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "diagnostics/system/lscpu.txt",
|
|
Content: []byte(lscpuContent),
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
result, err := p.Parse(files)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if len(result.Hardware.CPUs) == 0 {
|
|
t.Fatal("expected CPU info")
|
|
}
|
|
|
|
cpu := result.Hardware.CPUs[0]
|
|
if cpu.Model != "Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz" {
|
|
t.Errorf("Model = %v", cpu.Model)
|
|
}
|
|
|
|
if cpu.Cores != 8 {
|
|
t.Errorf("Cores = %v, want 8", cpu.Cores)
|
|
}
|
|
|
|
if cpu.Threads != 16 {
|
|
t.Errorf("Threads = %v, want 16", cpu.Threads)
|
|
}
|
|
|
|
if cpu.FrequencyMHz != 3400 {
|
|
t.Errorf("FrequencyMHz = %v, want 3400", cpu.FrequencyMHz)
|
|
}
|
|
}
|
|
|
|
func TestParse_Memory(t *testing.T) {
|
|
memContent := ` total used free shared buff/cache available
|
|
Mem: 50Gi 11Gi 1.4Gi 565Mi 39Gi 39Gi
|
|
Swap: 0B 0B 0B
|
|
Total: 50Gi 11Gi 1.4Gi
|
|
`
|
|
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "diagnostics/system/memory.txt",
|
|
Content: []byte(memContent),
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
result, err := p.Parse(files)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if len(result.Hardware.Memory) == 0 {
|
|
t.Fatal("expected memory info")
|
|
}
|
|
|
|
mem := result.Hardware.Memory[0]
|
|
expectedSizeMB := 50 * 1024 // 50 GiB in MB
|
|
|
|
if mem.SizeMB != expectedSizeMB {
|
|
t.Errorf("SizeMB = %v, want %v", mem.SizeMB, expectedSizeMB)
|
|
}
|
|
|
|
if mem.Type != "DRAM" {
|
|
t.Errorf("Type = %v, want 'DRAM'", mem.Type)
|
|
}
|
|
}
|
|
|
|
func TestParse_SMART(t *testing.T) {
|
|
smartContent := `smartctl 7.5 2025-04-30 r5714 [x86_64-linux-6.12.54-Unraid] (local build)
|
|
Copyright (C) 2002-25, Bruce Allen, Christian Franke, www.smartmontools.org
|
|
|
|
=== START OF INFORMATION SECTION ===
|
|
Device Model: ST4000NM000B-2TF100
|
|
Serial Number: WX103EC9
|
|
LU WWN Device Id: 5 000c50 0ed59db60
|
|
Firmware Version: TNA1
|
|
User Capacity: 4,000,787,030,016 bytes [4.00 TB]
|
|
Sector Size: 512 bytes logical/physical
|
|
Rotation Rate: 7200 rpm
|
|
Form Factor: 3.5 inches
|
|
SATA Version is: SATA 3.3, 6.0 Gb/s (current: 6.0 Gb/s)
|
|
|
|
=== START OF READ SMART DATA SECTION ===
|
|
SMART overall-health self-assessment test result: PASSED
|
|
`
|
|
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "diagnostics/smart/ST4000NM000B-2TF100_WX103EC9-20260205-2333 disk1 (sdi).txt",
|
|
Content: []byte(smartContent),
|
|
},
|
|
}
|
|
|
|
p := &Parser{}
|
|
result, err := p.Parse(files)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if len(result.Hardware.Storage) == 0 {
|
|
t.Fatal("expected storage info")
|
|
}
|
|
|
|
disk := result.Hardware.Storage[0]
|
|
|
|
if disk.Model != "ST4000NM000B-2TF100" {
|
|
t.Errorf("Model = %v, want 'ST4000NM000B-2TF100'", disk.Model)
|
|
}
|
|
|
|
if disk.SerialNumber != "WX103EC9" {
|
|
t.Errorf("SerialNumber = %v, want 'WX103EC9'", disk.SerialNumber)
|
|
}
|
|
|
|
if disk.Firmware != "TNA1" {
|
|
t.Errorf("Firmware = %v, want 'TNA1'", disk.Firmware)
|
|
}
|
|
|
|
if disk.SizeGB != 4000 {
|
|
t.Errorf("SizeGB = %v, want 4000", disk.SizeGB)
|
|
}
|
|
|
|
if disk.Type != "hdd" {
|
|
t.Errorf("Type = %v, want 'hdd'", disk.Type)
|
|
}
|
|
|
|
// Check that no health warnings were generated (PASSED health)
|
|
healthWarnings := 0
|
|
for _, event := range result.Events {
|
|
if event.EventType == "Disk Health" && event.Severity == "warning" {
|
|
healthWarnings++
|
|
}
|
|
}
|
|
if healthWarnings != 0 {
|
|
t.Errorf("Expected no health warnings for PASSED disk, got %v", healthWarnings)
|
|
}
|
|
}
|
|
|
|
func TestParser_Metadata(t *testing.T) {
|
|
p := &Parser{}
|
|
|
|
if p.Name() != "Unraid Parser" {
|
|
t.Errorf("Name() = %v, want 'Unraid Parser'", p.Name())
|
|
}
|
|
|
|
if p.Vendor() != "unraid" {
|
|
t.Errorf("Vendor() = %v, want 'unraid'", p.Vendor())
|
|
}
|
|
|
|
if p.Version() == "" {
|
|
t.Error("Version() should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestParse_MemoryDIMMsFromMeminfo(t *testing.T) {
|
|
memInfo := `MemTotal: 53393436 kB
|
|
|
|
Handle 0x002D, DMI type 17, 34 bytes
|
|
Memory Device
|
|
Size: 16 GB
|
|
Locator: Node0_Dimm1
|
|
Bank Locator: Node0_Bank0
|
|
Type: DDR3
|
|
Speed: 1333 MT/s
|
|
Manufacturer: Samsung
|
|
Serial Number: 238F7649
|
|
Part Number: M393B2G70BH0-
|
|
Rank: 4
|
|
Configured Memory Speed: 1333 MT/s
|
|
|
|
Handle 0x002F, DMI type 17, 34 bytes
|
|
Memory Device
|
|
Size: No Module Installed
|
|
Locator: Node0_Dimm2
|
|
`
|
|
|
|
files := []parser.ExtractedFile{
|
|
{Path: "diagnostics/system/memory.txt", Content: []byte("Mem: 50Gi")},
|
|
{Path: "diagnostics/system/meminfo.txt", Content: []byte(memInfo)},
|
|
}
|
|
|
|
p := &Parser{}
|
|
result, err := p.Parse(files)
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if got := len(result.Hardware.Memory); got != 1 {
|
|
t.Fatalf("expected only installed DIMM entries, got %d entries", got)
|
|
}
|
|
dimm := result.Hardware.Memory[0]
|
|
if dimm.Slot != "Node0_Dimm1" {
|
|
t.Errorf("Slot = %q, want Node0_Dimm1", dimm.Slot)
|
|
}
|
|
if dimm.SizeMB != 16*1024 {
|
|
t.Errorf("SizeMB = %d, want %d", dimm.SizeMB, 16*1024)
|
|
}
|
|
if dimm.Type != "DDR3" {
|
|
t.Errorf("Type = %q, want DDR3", dimm.Type)
|
|
}
|
|
if dimm.SerialNumber != "238F7649" {
|
|
t.Errorf("SerialNumber = %q, want 238F7649", dimm.SerialNumber)
|
|
}
|
|
}
|
|
|
|
func TestParse_NetworkAndPCIeFromLSPCIAndEthtool(t *testing.T) {
|
|
lspci := `03:00.0 SCSI storage controller [0100]: Broadcom / LSI SAS2008 PCI-Express Fusion-MPT SAS-2 [Falcon] [1000:0072] (rev 03)
|
|
07:00.0 Ethernet controller [0200]: Realtek Semiconductor Co., Ltd. RTL8111/8168/8211/8411 PCI Express Gigabit Ethernet Controller [10ec:8168] (rev 06)
|
|
`
|
|
ethtool := `Settings for eth0:
|
|
Speed: 1000Mb/s
|
|
Link detected: yes
|
|
driver: r8168
|
|
firmware-version:
|
|
bus-info: 0000:07:00.0
|
|
--------------------------------
|
|
`
|
|
files := []parser.ExtractedFile{
|
|
{Path: "diagnostics/system/lspci.txt", Content: []byte(lspci)},
|
|
{Path: "diagnostics/system/ethtool.txt", Content: []byte(ethtool)},
|
|
}
|
|
|
|
p := &Parser{}
|
|
result, err := p.Parse(files)
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if len(result.Hardware.NetworkAdapters) != 1 {
|
|
t.Fatalf("expected 1 network adapter, got %d", len(result.Hardware.NetworkAdapters))
|
|
}
|
|
nic := result.Hardware.NetworkAdapters[0]
|
|
if nic.Location != "0000:07:00.0" {
|
|
t.Errorf("Location = %q, want 0000:07:00.0", nic.Location)
|
|
}
|
|
if nic.Model == "" {
|
|
t.Error("Model should not be empty")
|
|
}
|
|
if nic.Vendor == "" {
|
|
t.Error("Vendor should not be empty")
|
|
}
|
|
|
|
if len(result.Hardware.PCIeDevices) < 2 {
|
|
t.Fatalf("expected at least 2 PCIe devices, got %d", len(result.Hardware.PCIeDevices))
|
|
}
|
|
}
|
|
|
|
func TestParse_HostSerialFallbackFromVarsUUID(t *testing.T) {
|
|
vars := ` [flashGUID] => 1...
|
|
[regGUID] => 1...7
|
|
[uuid] => 2713440667722491190
|
|
`
|
|
files := []parser.ExtractedFile{
|
|
{Path: "diagnostics/system/vars.txt", Content: []byte(vars)},
|
|
}
|
|
|
|
p := &Parser{}
|
|
result, err := p.Parse(files)
|
|
if err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
if result.Hardware.BoardInfo.SerialNumber != "2713440667722491190" {
|
|
t.Fatalf("BoardInfo.SerialNumber = %q, want vars uuid", result.Hardware.BoardInfo.SerialNumber)
|
|
}
|
|
if result.Hardware.BoardInfo.UUID != "2713440667722491190" {
|
|
t.Fatalf("BoardInfo.UUID = %q, want vars uuid", result.Hardware.BoardInfo.UUID)
|
|
}
|
|
}
|