diff --git a/internal/sensors/parser_test.go b/internal/sensors/parser_test.go new file mode 100644 index 000000000..d7827ad1b --- /dev/null +++ b/internal/sensors/parser_test.go @@ -0,0 +1,461 @@ +package sensors + +import ( + "math" + "testing" +) + +func TestParse_IntelCPU(t *testing.T) { + // Typical Intel CPU output from sensors -j + intelJSON := `{ + "coretemp-isa-0000": { + "Adapter": "ISA adapter", + "Package id 0": { + "temp1_input": 45.000, + "temp1_max": 100.000, + "temp1_crit": 100.000, + "temp1_crit_alarm": 0.000 + }, + "Core 0": { + "temp2_input": 43.000, + "temp2_max": 100.000, + "temp2_crit": 100.000, + "temp2_crit_alarm": 0.000 + }, + "Core 1": { + "temp3_input": 44.000, + "temp3_max": 100.000, + "temp3_crit": 100.000, + "temp3_crit_alarm": 0.000 + }, + "Core 2": { + "temp4_input": 42.000, + "temp4_max": 100.000, + "temp4_crit": 100.000, + "temp4_crit_alarm": 0.000 + }, + "Core 3": { + "temp5_input": 45.000, + "temp5_max": 100.000, + "temp5_crit": 100.000, + "temp5_crit_alarm": 0.000 + } + } + }` + + data, err := Parse(intelJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } + + if data.CPUPackage != 45.0 { + t.Errorf("CPUPackage = %v, want 45.0", data.CPUPackage) + } + + if data.CPUMax != 45.0 { + t.Errorf("CPUMax = %v, want 45.0", data.CPUMax) + } + + if len(data.Cores) != 4 { + t.Errorf("len(Cores) = %d, want 4", len(data.Cores)) + } + + if data.Cores["Core 0"] != 43.0 { + t.Errorf("Core 0 = %v, want 43.0", data.Cores["Core 0"]) + } +} + +func TestParse_AMDCPU(t *testing.T) { + // Typical AMD Ryzen output from sensors -j + amdJSON := `{ + "k10temp-pci-00c3": { + "Adapter": "PCI adapter", + "Tctl": { + "temp1_input": 52.000 + }, + "Tdie": { + "temp2_input": 52.000 + }, + "Tccd1": { + "temp3_input": 48.000 + }, + "Tccd2": { + "temp4_input": 50.000 + } + } + }` + + data, err := Parse(amdJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } + + // Should use Tdie as package temp + if data.CPUPackage != 52.0 { + t.Errorf("CPUPackage = %v, want 52.0", data.CPUPackage) + } + + if data.CPUMax != 52.0 { + t.Errorf("CPUMax = %v, want 52.0", data.CPUMax) + } +} + +func TestParse_NVMe(t *testing.T) { + nvmeJSON := `{ + "nvme-pci-0100": { + "Adapter": "PCI adapter", + "Composite": { + "temp1_input": 38.850, + "temp1_max": 81.850, + "temp1_min": -273.150, + "temp1_crit": 84.850, + "temp1_alarm": 0.000 + }, + "Sensor 1": { + "temp2_input": 38.850, + "temp2_max": 65261.850, + "temp2_min": -273.150 + } + }, + "nvme-pci-0200": { + "Adapter": "PCI adapter", + "Composite": { + "temp1_input": 42.000 + } + } + }` + + data, err := Parse(nvmeJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } + + if len(data.NVMe) != 2 { + t.Errorf("len(NVMe) = %d, want 2", len(data.NVMe)) + } + + if data.NVMe["nvme-pci-0100"] != 38.85 { + t.Errorf("nvme-pci-0100 = %v, want 38.85", data.NVMe["nvme-pci-0100"]) + } + + if data.NVMe["nvme-pci-0200"] != 42.0 { + t.Errorf("nvme-pci-0200 = %v, want 42.0", data.NVMe["nvme-pci-0200"]) + } +} + +func TestParse_GPU(t *testing.T) { + gpuJSON := `{ + "amdgpu-pci-0300": { + "Adapter": "PCI adapter", + "edge": { + "temp1_input": 55.000 + }, + "junction": { + "temp2_input": 58.000 + }, + "mem": { + "temp3_input": 52.000 + } + } + }` + + data, err := Parse(gpuJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } + + if len(data.GPU) != 3 { + t.Errorf("len(GPU) = %d, want 3", len(data.GPU)) + } + + // Check edge temp + edgeKey := "amdgpu-pci-0300_edge" + if data.GPU[edgeKey] != 55.0 { + t.Errorf("GPU[%s] = %v, want 55.0", edgeKey, data.GPU[edgeKey]) + } +} + +func TestParse_Combined(t *testing.T) { + // Combined CPU + NVMe + GPU output + combinedJSON := `{ + "coretemp-isa-0000": { + "Package id 0": {"temp1_input": 50.000}, + "Core 0": {"temp2_input": 48.000} + }, + "nvme-pci-0100": { + "Composite": {"temp1_input": 40.000} + }, + "amdgpu-pci-0300": { + "edge": {"temp1_input": 60.000} + } + }` + + data, err := Parse(combinedJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } + + if data.CPUPackage != 50.0 { + t.Errorf("CPUPackage = %v, want 50.0", data.CPUPackage) + } + + if len(data.NVMe) != 1 { + t.Errorf("len(NVMe) = %d, want 1", len(data.NVMe)) + } + + if len(data.GPU) != 1 { + t.Errorf("len(GPU) = %d, want 1", len(data.GPU)) + } +} + +func TestParse_Empty(t *testing.T) { + _, err := Parse("") + if err == nil { + t.Error("Parse() should fail on empty input") + } + + _, err = Parse(" ") + if err == nil { + t.Error("Parse() should fail on whitespace-only input") + } +} + +func TestParse_InvalidJSON(t *testing.T) { + _, err := Parse("not valid json") + if err == nil { + t.Error("Parse() should fail on invalid JSON") + } + + _, err = Parse("{incomplete") + if err == nil { + t.Error("Parse() should fail on incomplete JSON") + } +} + +func TestParse_EmptyObject(t *testing.T) { + data, err := Parse("{}") + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if data.Available { + t.Error("Expected Available to be false for empty object") + } +} + +func TestParse_NoRecognizedSensors(t *testing.T) { + // JSON with unknown sensor types + unknownJSON := `{ + "unknown-chip-0000": { + "Adapter": "Unknown adapter", + "some_sensor": {"temp1_input": 50.000} + } + }` + + data, err := Parse(unknownJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if data.Available { + t.Error("Expected Available to be false for unknown sensors") + } +} + +func TestParse_SuperIOChip(t *testing.T) { + // NCT6775 SuperIO chip output + superioJSON := `{ + "nct6775-isa-0290": { + "Adapter": "ISA adapter", + "CPUTIN": { + "temp1_input": 45.000, + "temp1_max": 80.000, + "temp1_max_hyst": 75.000 + }, + "SYSTIN": { + "temp2_input": 35.000 + } + } + }` + + data, err := Parse(superioJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } + + if data.CPUPackage != 45.0 { + t.Errorf("CPUPackage = %v, want 45.0", data.CPUPackage) + } +} + +func TestParse_RaspberryPi(t *testing.T) { + // Raspberry Pi thermal zone output + rpiJSON := `{ + "cpu_thermal-virtual-0": { + "Adapter": "Virtual device", + "temp1": { + "temp1_input": 52.000 + } + } + }` + + data, err := Parse(rpiJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } +} + +func TestIsCPUChip(t *testing.T) { + tests := []struct { + chip string + expected bool + }{ + {"coretemp-isa-0000", true}, + {"k10temp-pci-00c3", true}, + {"zenpower-pci-00c3", true}, + {"nct6775-isa-0290", true}, + {"cpu_thermal-virtual-0", true}, + {"acpitz-acpi-0", true}, + {"nvme-pci-0100", false}, + {"amdgpu-pci-0300", false}, + {"unknown-chip", false}, + } + + for _, tc := range tests { + t.Run(tc.chip, func(t *testing.T) { + result := isCPUChip(tc.chip) + if result != tc.expected { + t.Errorf("isCPUChip(%q) = %v, want %v", tc.chip, result, tc.expected) + } + }) + } +} + +func TestExtractTempInput(t *testing.T) { + tests := []struct { + name string + input map[string]interface{} + expected float64 + isNaN bool + }{ + { + name: "float value", + input: map[string]interface{}{"temp1_input": 45.5}, + expected: 45.5, + }, + { + name: "int value", + input: map[string]interface{}{"temp1_input": 45}, + expected: 45.0, + }, + { + name: "no input field", + input: map[string]interface{}{"temp1_max": 100.0}, + isNaN: true, + }, + { + name: "empty map", + input: map[string]interface{}{}, + isNaN: true, + }, + { + name: "wrong suffix", + input: map[string]interface{}{"temp1_max": 45.0}, + isNaN: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := extractTempInput(tc.input) + if tc.isNaN { + if !math.IsNaN(result) { + t.Errorf("extractTempInput() = %v, want NaN", result) + } + } else { + if result != tc.expected { + t.Errorf("extractTempInput() = %v, want %v", result, tc.expected) + } + } + }) + } +} + +func TestParse_MaxTempCalculation(t *testing.T) { + // Test that CPUMax is correctly calculated from cores when no package temp + coresOnlyJSON := `{ + "coretemp-isa-0000": { + "Core 0": {"temp2_input": 40.000}, + "Core 1": {"temp3_input": 45.000}, + "Core 2": {"temp4_input": 42.000}, + "Core 3": {"temp5_input": 48.000} + } + }` + + data, err := Parse(coresOnlyJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + // Should use max core temp as package temp + if data.CPUPackage != 48.0 { + t.Errorf("CPUPackage = %v, want 48.0 (max core temp)", data.CPUPackage) + } + + if data.CPUMax != 48.0 { + t.Errorf("CPUMax = %v, want 48.0", data.CPUMax) + } +} + +func TestParse_NouveauGPU(t *testing.T) { + // Nouveau (open-source NVIDIA) driver output + nouveauJSON := `{ + "nouveau-pci-0100": { + "Adapter": "PCI adapter", + "temp1": { + "temp1_input": 45.000 + } + } + }` + + data, err := Parse(nouveauJSON) + if err != nil { + t.Fatalf("Parse() error: %v", err) + } + + if !data.Available { + t.Error("Expected Available to be true") + } + + if len(data.GPU) != 1 { + t.Errorf("len(GPU) = %d, want 1", len(data.GPU)) + } +}