mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-13 15:28:38 +00:00
Add focused unit tests for four utility functions in temperature.go: - extractTempInput: 16 test cases for sensor value extraction - extractCoreNumber: 18 test cases for core number parsing - extractHostname: 21 test cases for URL hostname extraction - normalizeSMARTEntries: 15 test cases for SMART data normalization 70 test cases total covering type conversions, edge cases, boundary conditions, and error handling paths.
1332 lines
34 KiB
Go
1332 lines
34 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
|
)
|
|
|
|
type stubProxyResponse struct {
|
|
output string
|
|
err error
|
|
}
|
|
|
|
type stubTemperatureProxy struct {
|
|
mu sync.Mutex
|
|
available bool
|
|
responses []stubProxyResponse
|
|
responseFunc func(call int) stubProxyResponse
|
|
callCount int
|
|
}
|
|
|
|
func (s *stubTemperatureProxy) IsAvailable() bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.available
|
|
}
|
|
|
|
func (s *stubTemperatureProxy) GetTemperature(host string) (string, error) {
|
|
s.mu.Lock()
|
|
call := s.callCount
|
|
s.callCount++
|
|
|
|
resp := stubProxyResponse{}
|
|
switch {
|
|
case call < len(s.responses):
|
|
resp = s.responses[call]
|
|
case s.responseFunc != nil:
|
|
resp = s.responseFunc(call)
|
|
case len(s.responses) > 0:
|
|
resp = s.responses[len(s.responses)-1]
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
return resp.output, resp.err
|
|
}
|
|
|
|
func (s *stubTemperatureProxy) setAvailable(v bool) {
|
|
s.mu.Lock()
|
|
s.available = v
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func TestParseSensorsJSON_NoTemperatureData(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
// Test with a chip that doesn't match any known CPU or NVMe patterns
|
|
jsonStr := `{
|
|
"unknown-sensor-0": {
|
|
"Adapter": "Unknown interface",
|
|
"temp1": {
|
|
"temp1_label": "temp1"
|
|
}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if temp.Available {
|
|
t.Fatalf("expected temperature to be unavailable when no CPU or NVMe chips are detected")
|
|
}
|
|
if temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be false when no CPU chip detected")
|
|
}
|
|
if temp.HasNVMe {
|
|
t.Fatalf("expected HasNVMe to be false when no NVMe chip detected")
|
|
}
|
|
}
|
|
|
|
func TestParseSensorsJSON_WithCpuAndNvmeData(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"coretemp-isa-0000": {
|
|
"Package id 0": {"temp1_input": 45.5},
|
|
"Core 0": {"temp2_input": 43.0},
|
|
"Core 1": {"temp3_input": 44.2}
|
|
},
|
|
"nvme-pci-0400": {
|
|
"Composite": {"temp1_input": 38.75}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when readings are present")
|
|
}
|
|
if temp.CPUPackage != 45.5 {
|
|
t.Fatalf("expected cpu package temperature 45.5, got %.2f", temp.CPUPackage)
|
|
}
|
|
if temp.CPUMax <= 0 {
|
|
t.Fatalf("expected cpu max temperature to be greater than zero, got %.2f", temp.CPUMax)
|
|
}
|
|
if len(temp.Cores) != 2 {
|
|
t.Fatalf("expected two core temperatures, got %d", len(temp.Cores))
|
|
}
|
|
if len(temp.NVMe) != 1 {
|
|
t.Fatalf("expected one NVMe temperature, got %d", len(temp.NVMe))
|
|
}
|
|
if temp.NVMe[0].Temp != 38.75 {
|
|
t.Fatalf("expected NVMe temperature 38.75, got %.2f", temp.NVMe[0].Temp)
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true when CPU data present")
|
|
}
|
|
if !temp.HasNVMe {
|
|
t.Fatalf("expected HasNVMe to be true when NVMe data present")
|
|
}
|
|
}
|
|
|
|
func TestParseSensorsJSON_WithAmdTctlOnly(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"k10temp-pci-00c3": {
|
|
"Tctl": {"temp1_input": 55.4}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when Tctl reading present")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true when AMD Tctl is present")
|
|
}
|
|
if temp.CPUPackage != 55.4 {
|
|
t.Fatalf("expected cpu package temperature 55.4, got %.2f", temp.CPUPackage)
|
|
}
|
|
if temp.CPUMax != 55.4 {
|
|
t.Fatalf("expected cpu max temperature to follow Tctl value, got %.2f", temp.CPUMax)
|
|
}
|
|
}
|
|
|
|
func TestParseSensorsJSON_RPiWrapper(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{"rpitemp-virtual":{"temp1":{"temp1_input":47.5}}}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing wrapper output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true for wrapper output")
|
|
}
|
|
if temp.CPUPackage != 47.5 {
|
|
t.Fatalf("expected cpu package temperature 47.5, got %.2f", temp.CPUPackage)
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available for wrapper output")
|
|
}
|
|
}
|
|
|
|
func TestParseSensorsJSON_SMARTWithNullTemperature(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
lastUpdated := time.Now().UTC().Truncate(time.Second).Format(time.RFC3339)
|
|
jsonStr := fmt.Sprintf(`{
|
|
"sensors": {
|
|
"coretemp-isa-0000": {
|
|
"Package id 0": {"temp1_input": 55.0}
|
|
}
|
|
},
|
|
"smart": [
|
|
{
|
|
"device": "/dev/sda",
|
|
"serial": "S1",
|
|
"wwn": "WWN1",
|
|
"model": "Model1",
|
|
"type": "sat",
|
|
"temperature": 34,
|
|
"lastUpdated": "%s",
|
|
"standbySkipped": false
|
|
},
|
|
{
|
|
"device": "/dev/zd0",
|
|
"temperature": null,
|
|
"standbySkipped": true
|
|
}
|
|
]
|
|
}`, lastUpdated)
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing SMART wrapper output: %v", err)
|
|
}
|
|
|
|
if temp == nil || !temp.Available {
|
|
t.Fatalf("expected temperature data to be available when SMART data present")
|
|
}
|
|
if !temp.HasSMART {
|
|
t.Fatalf("expected HasSMART to be true when SMART data present")
|
|
}
|
|
if len(temp.SMART) != 2 {
|
|
t.Fatalf("expected two SMART entries, got %d", len(temp.SMART))
|
|
}
|
|
if temp.SMART[0].Temperature != 34 {
|
|
t.Fatalf("expected first SMART temperature 34, got %d", temp.SMART[0].Temperature)
|
|
}
|
|
if temp.SMART[0].LastUpdated.IsZero() {
|
|
t.Fatalf("expected first SMART entry to include parsed lastUpdated timestamp")
|
|
}
|
|
if temp.SMART[1].Temperature != 0 {
|
|
t.Fatalf("expected standby SMART entry to default to temperature 0, got %d", temp.SMART[1].Temperature)
|
|
}
|
|
if !temp.SMART[1].StandbySkipped {
|
|
t.Fatalf("expected standbySkipped to be true for second SMART entry")
|
|
}
|
|
}
|
|
|
|
func TestShouldDisableProxy(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
if !collector.shouldDisableProxy(fmt.Errorf("plain")) {
|
|
t.Fatalf("expected plain errors to disable proxy")
|
|
}
|
|
|
|
transportErr := &tempproxy.ProxyError{Type: tempproxy.ErrorTypeTransport}
|
|
if !collector.shouldDisableProxy(transportErr) {
|
|
t.Fatalf("expected transport errors to disable proxy")
|
|
}
|
|
|
|
sensorErr := &tempproxy.ProxyError{Type: tempproxy.ErrorTypeSensor}
|
|
if collector.shouldDisableProxy(sensorErr) {
|
|
t.Fatalf("sensor errors should not disable proxy")
|
|
}
|
|
}
|
|
|
|
// TestParseSensorsJSON_NVMeOnly tests that NVMe-only systems don't show "No CPU sensor"
|
|
func TestParseSensorsJSON_NVMeOnly(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"nvme-pci-0400": {
|
|
"Composite": {"temp1_input": 42.5}
|
|
},
|
|
"nvme-pci-0500": {
|
|
"Composite": {"temp1_input": 38.0}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
// available should be true (any temperature data exists)
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when NVMe readings are present")
|
|
}
|
|
// hasCPU should be false (no CPU temperature data)
|
|
if temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be false when only NVMe data present")
|
|
}
|
|
// hasNVMe should be true
|
|
if !temp.HasNVMe {
|
|
t.Fatalf("expected HasNVMe to be true when NVMe data present")
|
|
}
|
|
// Verify NVMe data was parsed correctly
|
|
if len(temp.NVMe) != 2 {
|
|
t.Fatalf("expected two NVMe temperatures, got %d", len(temp.NVMe))
|
|
}
|
|
// Check that both expected temperatures are present (order may vary)
|
|
foundTemps := make(map[float64]bool)
|
|
for _, nvme := range temp.NVMe {
|
|
foundTemps[nvme.Temp] = true
|
|
}
|
|
if !foundTemps[42.5] {
|
|
t.Fatalf("expected to find NVMe temperature 42.5")
|
|
}
|
|
if !foundTemps[38.0] {
|
|
t.Fatalf("expected to find NVMe temperature 38.0")
|
|
}
|
|
}
|
|
|
|
// TestParseSensorsJSON_ZeroTemperature tests that HasCPU is true even when sensor reports 0°C
|
|
func TestParseSensorsJSON_ZeroTemperature(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"coretemp-isa-0000": {
|
|
"Package id 0": {"temp1_input": 0.0},
|
|
"Core 0": {"temp2_input": 0.0}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
// hasCPU should be true because coretemp chip was detected, even though values are 0
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true when CPU chip is detected (even with 0°C readings)")
|
|
}
|
|
// available should be true because we have a CPU sensor
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when CPU chip is detected")
|
|
}
|
|
// Values should be accepted (not filtered out)
|
|
if temp.CPUPackage != 0.0 {
|
|
t.Fatalf("expected CPUPackage to be 0.0, got %.2f", temp.CPUPackage)
|
|
}
|
|
if len(temp.Cores) != 1 {
|
|
t.Fatalf("expected one core temperature, got %d", len(temp.Cores))
|
|
}
|
|
if temp.Cores[0].Temp != 0.0 {
|
|
t.Fatalf("expected core temperature to be 0.0, got %.2f", temp.Cores[0].Temp)
|
|
}
|
|
}
|
|
|
|
func TestParseRPiTemperature(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
temp, err := collector.parseRPiTemperature("48720\n")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing RPi thermal zone output: %v", err)
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be marked available")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true for RPi thermal zone output")
|
|
}
|
|
expected := 48.72
|
|
if diff := temp.CPUPackage - expected; diff > 1e-6 || diff < -1e-6 {
|
|
t.Fatalf("expected cpu package temperature %.2f, got %.2f", expected, temp.CPUPackage)
|
|
}
|
|
if temp.CPUMax != temp.CPUPackage {
|
|
t.Fatalf("expected cpu max to match package temperature %.2f, got %.2f", temp.CPUPackage, temp.CPUMax)
|
|
}
|
|
if temp.LastUpdate.IsZero() {
|
|
t.Fatalf("expected LastUpdate to be set")
|
|
}
|
|
if elapsed := time.Since(temp.LastUpdate); elapsed > 2*time.Second {
|
|
t.Fatalf("expected LastUpdate to be recent, got %s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestParseSensorsJSON_PiPartialSensors(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"cpu_thermal-virtual-0": {
|
|
"Adapter": "Virtual device",
|
|
"temp1": {"temp1_input": 51.625}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing Pi sensors output: %v", err)
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when cpu_thermal sensor present")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true when cpu_thermal sensor present")
|
|
}
|
|
if temp.CPUPackage != 51.625 {
|
|
t.Fatalf("expected cpu package temperature 51.625, got %.3f", temp.CPUPackage)
|
|
}
|
|
if temp.CPUMax != 51.625 {
|
|
t.Fatalf("expected cpu max temperature 51.625, got %.3f", temp.CPUMax)
|
|
}
|
|
if len(temp.Cores) != 0 {
|
|
t.Fatalf("expected no per-core temperatures, got %d entries", len(temp.Cores))
|
|
}
|
|
}
|
|
|
|
func TestParseSensorsJSON_CoretempAndRPiFallback(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"coretemp-isa-0000": {
|
|
"Package id 0": {"temp1_input": 65.0},
|
|
"Core 0": {"temp2_input": 63.0},
|
|
"Core 1": {"temp3_input": 62.5}
|
|
},
|
|
"cpu_thermal-virtual-0": {
|
|
"temp1": {"temp1_input": 50.0}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing mixed sensors output: %v", err)
|
|
}
|
|
if temp.CPUPackage != 65.0 {
|
|
t.Fatalf("expected cpu package temperature 65.0 from coretemp, got %.2f", temp.CPUPackage)
|
|
}
|
|
if temp.CPUMax < 63.0 {
|
|
t.Fatalf("expected cpu max to reflect hottest core (>=63.0), got %.2f", temp.CPUMax)
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true when CPU sensors present")
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when CPU sensors present")
|
|
}
|
|
}
|
|
|
|
func TestTemperatureCollector_DisablesProxyAfterFailures(t *testing.T) {
|
|
stub := &stubTemperatureProxy{
|
|
responses: []stubProxyResponse{
|
|
{err: &tempproxy.ProxyError{Type: tempproxy.ErrorTypeTransport, Message: "transport failure 1"}},
|
|
{err: &tempproxy.ProxyError{Type: tempproxy.ErrorTypeTransport, Message: "transport failure 2"}},
|
|
{err: &tempproxy.ProxyError{Type: tempproxy.ErrorTypeTransport, Message: "transport failure 3"}},
|
|
},
|
|
}
|
|
stub.setAvailable(true)
|
|
|
|
collector := &TemperatureCollector{
|
|
proxyClient: stub,
|
|
useProxy: true,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
for i := 0; i < proxyFailureThreshold; i++ {
|
|
temp, err := collector.CollectTemperature(ctx, "https://node.example", "node")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error on proxy failure %d: %v", i+1, err)
|
|
}
|
|
if temp.Available {
|
|
t.Fatalf("expected temperature to be unavailable after proxy failure %d", i+1)
|
|
}
|
|
}
|
|
|
|
if collector.useProxy {
|
|
t.Fatalf("expected proxy to be disabled after %d failures", proxyFailureThreshold)
|
|
}
|
|
if collector.proxyFailures != 0 {
|
|
t.Fatalf("expected proxy failure counter to reset after disable, got %d", collector.proxyFailures)
|
|
}
|
|
if collector.proxyCooldownUntil.IsZero() {
|
|
t.Fatalf("expected proxy cooldown to be scheduled after disable")
|
|
}
|
|
if time.Until(collector.proxyCooldownUntil) <= 0 {
|
|
t.Fatalf("expected proxy cooldown to be in the future, got %s", collector.proxyCooldownUntil)
|
|
}
|
|
}
|
|
|
|
func TestTemperatureCollector_ProxyReenablesAfterCooldown(t *testing.T) {
|
|
stub := &stubTemperatureProxy{}
|
|
stub.setAvailable(true)
|
|
|
|
collector := &TemperatureCollector{
|
|
proxyClient: stub,
|
|
useProxy: false,
|
|
proxyCooldownUntil: time.Now().Add(-time.Minute),
|
|
}
|
|
|
|
if !collector.isProxyEnabled() {
|
|
t.Fatalf("expected proxy to re-enable when available after cooldown")
|
|
}
|
|
if !collector.useProxy {
|
|
t.Fatalf("expected useProxy to be true after proxy restored")
|
|
}
|
|
if !collector.proxyCooldownUntil.IsZero() {
|
|
t.Fatalf("expected cooldown to reset after proxy restoration, got %s", collector.proxyCooldownUntil)
|
|
}
|
|
if collector.proxyFailures != 0 {
|
|
t.Fatalf("expected proxy failure counter to reset after restoration, got %d", collector.proxyFailures)
|
|
}
|
|
}
|
|
|
|
func TestTemperatureCollector_ProxyCooldownExtendsWhenUnavailable(t *testing.T) {
|
|
stub := &stubTemperatureProxy{}
|
|
stub.setAvailable(false)
|
|
|
|
collector := &TemperatureCollector{
|
|
proxyClient: stub,
|
|
useProxy: false,
|
|
proxyCooldownUntil: time.Now().Add(-time.Minute),
|
|
}
|
|
|
|
before := time.Now()
|
|
if collector.isProxyEnabled() {
|
|
t.Fatalf("expected proxy to remain disabled while unavailable")
|
|
}
|
|
if collector.useProxy {
|
|
t.Fatalf("expected useProxy to remain false while proxy unavailable")
|
|
}
|
|
if !collector.proxyCooldownUntil.After(before) {
|
|
t.Fatalf("expected cooldown to be pushed into the future, got %s", collector.proxyCooldownUntil)
|
|
}
|
|
}
|
|
|
|
func TestTemperatureCollector_SuccessResetsFailureCount(t *testing.T) {
|
|
successJSON := `{"coretemp-isa-0000":{"Package id 0":{"temp1_input": 45.0}}}`
|
|
stub := &stubTemperatureProxy{
|
|
responses: []stubProxyResponse{
|
|
{err: &tempproxy.ProxyError{Type: tempproxy.ErrorTypeTransport, Message: "transient failure"}},
|
|
{output: successJSON},
|
|
},
|
|
}
|
|
stub.setAvailable(true)
|
|
|
|
collector := &TemperatureCollector{
|
|
proxyClient: stub,
|
|
useProxy: true,
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if temp, err := collector.CollectTemperature(ctx, "https://node.example", "node"); err != nil {
|
|
t.Fatalf("unexpected error during proxy failure: %v", err)
|
|
} else if temp.Available {
|
|
t.Fatalf("expected unavailable temperature on proxy failure")
|
|
}
|
|
if collector.proxyFailures != 1 {
|
|
t.Fatalf("expected proxy failure counter to increment to 1, got %d", collector.proxyFailures)
|
|
}
|
|
|
|
temp, err := collector.CollectTemperature(ctx, "https://node.example", "node")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error on proxy success: %v", err)
|
|
}
|
|
if temp == nil || !temp.Available {
|
|
t.Fatalf("expected valid temperature after proxy success")
|
|
}
|
|
if collector.proxyFailures != 0 {
|
|
t.Fatalf("expected proxy failure counter reset after success, got %d", collector.proxyFailures)
|
|
}
|
|
if !collector.useProxy {
|
|
t.Fatalf("expected proxy to remain enabled after success")
|
|
}
|
|
}
|
|
|
|
func TestTemperatureCollector_ConcurrentCollectTemperature(t *testing.T) {
|
|
successJSON := `{"coretemp-isa-0000":{"Package id 0":{"temp1_input": 55.0}}}`
|
|
var callCounter int32
|
|
stub := &stubTemperatureProxy{
|
|
responseFunc: func(int) stubProxyResponse {
|
|
n := atomic.AddInt32(&callCounter, 1)
|
|
if n%2 == 1 {
|
|
return stubProxyResponse{
|
|
err: &tempproxy.ProxyError{Type: tempproxy.ErrorTypeTransport, Message: "transient transport error"},
|
|
}
|
|
}
|
|
return stubProxyResponse{output: successJSON}
|
|
},
|
|
}
|
|
stub.setAvailable(true)
|
|
|
|
collector := &TemperatureCollector{
|
|
proxyClient: stub,
|
|
useProxy: true,
|
|
}
|
|
|
|
const goroutines = 16
|
|
const iterations = 32
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(goroutines)
|
|
|
|
ctx := context.Background()
|
|
for i := 0; i < goroutines; i++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < iterations; j++ {
|
|
temp, err := collector.CollectTemperature(ctx, "https://node.example", "node")
|
|
if err != nil {
|
|
t.Errorf("collect temperature returned error: %v", err)
|
|
return
|
|
}
|
|
if temp == nil {
|
|
t.Errorf("expected non-nil temperature result")
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
if !collector.useProxy {
|
|
t.Fatalf("expected proxy to remain enabled during concurrent collection")
|
|
}
|
|
if collector.proxyFailures >= proxyFailureThreshold {
|
|
t.Fatalf("expected proxy failures to stay below disable threshold, got %d", collector.proxyFailures)
|
|
}
|
|
}
|
|
|
|
func TestDisableLegacySSHOnAuthFailure(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
if !collector.disableLegacySSHOnAuthFailure(fmt.Errorf("ssh command failed: Permission denied (publickey)."), "node-1", "host-1") {
|
|
t.Fatalf("expected authentication errors to be detected")
|
|
}
|
|
// legacySSHDisabled check removed as we no longer globally disable SSH
|
|
|
|
// Repeated auth errors should still return true
|
|
if !collector.disableLegacySSHOnAuthFailure(fmt.Errorf("permission denied"), "node-1", "host-1") {
|
|
t.Fatalf("expected repeated authentication errors to be detected")
|
|
}
|
|
|
|
// Non-authentication errors should not trigger detection
|
|
if collector.disableLegacySSHOnAuthFailure(fmt.Errorf("connection timed out"), "node-1", "host-1") {
|
|
t.Fatalf("expected non-authentication errors to be ignored")
|
|
}
|
|
}
|
|
|
|
// TestParseSensorsJSON_NCT6687SuperIO tests NCT6687 SuperIO chip detection
|
|
func TestParseSensorsJSON_NCT6687SuperIO(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"nct6687-isa-0a20": {
|
|
"CPUTIN": {"temp1_input": 48.5},
|
|
"SYSTIN": {"temp2_input": 35.0}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing NCT6687 sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when NCT6687 CPUTIN is present")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true when NCT6687 chip is detected")
|
|
}
|
|
if temp.CPUPackage != 48.5 {
|
|
t.Fatalf("expected cpu package temperature 48.5 from CPUTIN, got %.2f", temp.CPUPackage)
|
|
}
|
|
}
|
|
|
|
// TestParseSensorsJSON_AmdChipletTemps tests AMD Tccd chiplet temperature detection
|
|
func TestParseSensorsJSON_AmdChipletTemps(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"k10temp-pci-00c3": {
|
|
"Tccd1": {"temp3_input": 62.5},
|
|
"Tccd2": {"temp4_input": 58.0}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing AMD chiplet sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available when AMD chiplet temps are present")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true when K10temp chip is detected")
|
|
}
|
|
// Should use highest chiplet temp as package temp
|
|
if temp.CPUPackage != 62.5 {
|
|
t.Fatalf("expected cpu package temperature to be highest chiplet temp (62.5), got %.2f", temp.CPUPackage)
|
|
}
|
|
// CPUMax should also be 62.5
|
|
if temp.CPUMax != 62.5 {
|
|
t.Fatalf("expected cpu max temperature 62.5, got %.2f", temp.CPUMax)
|
|
}
|
|
}
|
|
|
|
// TestParseSensorsJSON_AmdTctlAndChiplets tests AMD with both Tctl and chiplet temps
|
|
func TestParseSensorsJSON_AmdTctlAndChiplets(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"k10temp-pci-00c3": {
|
|
"Tctl": {"temp1_input": 65.0},
|
|
"Tccd1": {"temp3_input": 62.5},
|
|
"Tccd2": {"temp4_input": 58.0}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing AMD full sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true")
|
|
}
|
|
// Tctl should take precedence over chiplet temps for package temperature
|
|
if temp.CPUPackage != 65.0 {
|
|
t.Fatalf("expected cpu package temperature from Tctl (65.0), got %.2f", temp.CPUPackage)
|
|
}
|
|
// CPUMax should be Tctl since it's highest
|
|
if temp.CPUMax != 65.0 {
|
|
t.Fatalf("expected cpu max temperature 65.0, got %.2f", temp.CPUMax)
|
|
}
|
|
}
|
|
|
|
// TestParseSensorsJSON_MultipleSuperioCPUFields tests SuperIO chips with multiple CPU temp fields
|
|
func TestParseSensorsJSON_MultipleSuperioCPUFields(t *testing.T) {
|
|
collector := &TemperatureCollector{}
|
|
|
|
jsonStr := `{
|
|
"nct6775-isa-0290": {
|
|
"CPU Temperature": {"temp1_input": 52.0},
|
|
"SYSTIN": {"temp2_input": 38.0},
|
|
"AUXTIN0": {"temp3_input": 40.0}
|
|
}
|
|
}`
|
|
|
|
temp, err := collector.parseSensorsJSON(jsonStr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error parsing NCT6775 sensors output: %v", err)
|
|
}
|
|
if temp == nil {
|
|
t.Fatalf("expected temperature struct, got nil")
|
|
}
|
|
if !temp.Available {
|
|
t.Fatalf("expected temperature to be available")
|
|
}
|
|
if !temp.HasCPU {
|
|
t.Fatalf("expected HasCPU to be true")
|
|
}
|
|
if temp.CPUPackage != 52.0 {
|
|
t.Fatalf("expected cpu package temperature from 'CPU Temperature' field (52.0), got %.2f", temp.CPUPackage)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Unit tests for utility functions
|
|
// =============================================================================
|
|
|
|
func TestExtractTempInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sensorMap map[string]interface{}
|
|
wantTemp float64
|
|
wantNaN bool
|
|
}{
|
|
{
|
|
name: "float64 temp1_input",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": 45.5,
|
|
},
|
|
wantTemp: 45.5,
|
|
},
|
|
{
|
|
name: "float64 temp2_input",
|
|
sensorMap: map[string]interface{}{
|
|
"temp2_input": 72.3,
|
|
},
|
|
wantTemp: 72.3,
|
|
},
|
|
{
|
|
name: "int value converted to float64",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": 55,
|
|
},
|
|
wantTemp: 55.0,
|
|
},
|
|
{
|
|
name: "string value parseable",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": "62.5",
|
|
},
|
|
wantTemp: 62.5,
|
|
},
|
|
{
|
|
name: "string value non-numeric",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": "N/A",
|
|
},
|
|
wantNaN: true,
|
|
},
|
|
{
|
|
name: "no _input suffix",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1": 45.5,
|
|
"temp1_max": 100.0,
|
|
},
|
|
wantNaN: true,
|
|
},
|
|
{
|
|
name: "empty map",
|
|
sensorMap: map[string]interface{}{},
|
|
wantNaN: true,
|
|
},
|
|
{
|
|
name: "nil map",
|
|
sensorMap: nil,
|
|
wantNaN: true,
|
|
},
|
|
{
|
|
name: "zero temperature",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": 0.0,
|
|
},
|
|
wantTemp: 0.0,
|
|
},
|
|
{
|
|
name: "negative temperature",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": -10.5,
|
|
},
|
|
wantTemp: -10.5,
|
|
},
|
|
{
|
|
name: "mixed valid and invalid fields",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1": 45.0,
|
|
"temp1_input": 50.0,
|
|
"temp1_max": 100.0,
|
|
},
|
|
wantTemp: 50.0,
|
|
},
|
|
{
|
|
name: "boolean value (invalid type)",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": true,
|
|
},
|
|
wantNaN: true,
|
|
},
|
|
{
|
|
name: "nil value",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": nil,
|
|
},
|
|
wantNaN: true,
|
|
},
|
|
{
|
|
name: "very high temperature",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": 125.5,
|
|
},
|
|
wantTemp: 125.5,
|
|
},
|
|
{
|
|
name: "fractional precision",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_input": 45.123456789,
|
|
},
|
|
wantTemp: 45.123456789,
|
|
},
|
|
{
|
|
name: "temp_crit_input also matches",
|
|
sensorMap: map[string]interface{}{
|
|
"temp1_crit_input": 95.0,
|
|
},
|
|
wantTemp: 95.0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractTempInput(tt.sensorMap)
|
|
|
|
if tt.wantNaN {
|
|
if !math.IsNaN(got) {
|
|
t.Errorf("extractTempInput() = %v, want NaN", got)
|
|
}
|
|
return
|
|
}
|
|
|
|
if got != tt.wantTemp {
|
|
t.Errorf("extractTempInput() = %v, want %v", got, tt.wantTemp)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractCoreNumber(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
want int
|
|
}{
|
|
{"Core 0", 0},
|
|
{"Core 1", 1},
|
|
{"Core 10", 10},
|
|
{"Core 99", 99},
|
|
{"Core 127", 127},
|
|
{"Core", 0}, // missing number
|
|
{"Core ", 0}, // trailing space, no number
|
|
{"core 5", 5}, // lowercase
|
|
{"CORE 7", 7}, // uppercase
|
|
{"Core 12", 12}, // extra space (Fields handles this)
|
|
{"", 0}, // empty string
|
|
{" ", 0}, // whitespace only
|
|
{"Core abc", 0}, // non-numeric
|
|
{"Package id 0", 0}, // last part is "0"
|
|
{"temp1", 0}, // no spaces
|
|
{"Core 1000", 1000}, // large core number
|
|
{"Prefix Core 5", 5}, // core not at start
|
|
{"Core 0 extra", 0}, // text after number - "extra" is last field
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractCoreNumber(tt.name)
|
|
if got != tt.want {
|
|
t.Errorf("extractCoreNumber(%q) = %v, want %v", tt.name, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractHostname(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostURL string
|
|
want string
|
|
}{
|
|
{
|
|
name: "https with port",
|
|
hostURL: "https://192.168.1.100:8006",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "https without port",
|
|
hostURL: "https://192.168.1.100",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "http with port",
|
|
hostURL: "http://192.168.1.100:8006",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "http without port",
|
|
hostURL: "http://192.168.1.100",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "hostname with port",
|
|
hostURL: "https://proxmox.local:8006",
|
|
want: "proxmox.local",
|
|
},
|
|
{
|
|
name: "hostname without port",
|
|
hostURL: "https://proxmox.local",
|
|
want: "proxmox.local",
|
|
},
|
|
{
|
|
name: "bare IP",
|
|
hostURL: "192.168.1.100",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "bare IP with port",
|
|
hostURL: "192.168.1.100:8006",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "bare hostname",
|
|
hostURL: "proxmox.local",
|
|
want: "proxmox.local",
|
|
},
|
|
{
|
|
name: "bare hostname with port",
|
|
hostURL: "proxmox.local:8006",
|
|
want: "proxmox.local",
|
|
},
|
|
{
|
|
name: "with path",
|
|
hostURL: "https://192.168.1.100:8006/api2/json",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "empty string",
|
|
hostURL: "",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "protocol only",
|
|
hostURL: "https://",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "FQDN",
|
|
hostURL: "https://pve1.example.com:8006",
|
|
want: "pve1.example.com",
|
|
},
|
|
{
|
|
name: "localhost",
|
|
hostURL: "http://localhost:8006",
|
|
want: "localhost",
|
|
},
|
|
{
|
|
name: "127.0.0.1",
|
|
hostURL: "https://127.0.0.1:8006",
|
|
want: "127.0.0.1",
|
|
},
|
|
{
|
|
name: "uppercase protocol not stripped",
|
|
hostURL: "HTTPS://192.168.1.100:8006",
|
|
want: "HTTPS", // TrimPrefix is case-sensitive, so "HTTPS:" becomes hostname part
|
|
},
|
|
{
|
|
name: "trailing slash",
|
|
hostURL: "https://192.168.1.100/",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "query string",
|
|
hostURL: "https://192.168.1.100:8006/api?key=value",
|
|
want: "192.168.1.100",
|
|
},
|
|
{
|
|
name: "double protocol",
|
|
hostURL: "https://https://192.168.1.100",
|
|
want: "https",
|
|
},
|
|
{
|
|
name: "port only",
|
|
hostURL: ":8006",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := extractHostname(tt.hostURL)
|
|
if got != tt.want {
|
|
t.Errorf("extractHostname(%q) = %q, want %q", tt.hostURL, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSMARTEntries(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
raw []smartEntryRaw
|
|
want []models.DiskTemp
|
|
}{
|
|
{
|
|
name: "nil input",
|
|
raw: nil,
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "empty slice",
|
|
raw: []smartEntryRaw{},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "single entry with all fields",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: "/dev/sda",
|
|
Serial: "WD-WMC1T0123456",
|
|
WWN: "5 0014ee 2b1234567",
|
|
Model: "WDC WD40EFRX-68N32N0",
|
|
Type: "sata",
|
|
Temperature: intPtr(38),
|
|
LastUpdated: "2024-01-15T10:30:00Z",
|
|
StandbySkipped: false,
|
|
},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{
|
|
Device: "/dev/sda",
|
|
Serial: "WD-WMC1T0123456",
|
|
WWN: "5 0014ee 2b1234567",
|
|
Model: "WDC WD40EFRX-68N32N0",
|
|
Type: "sata",
|
|
Temperature: 38,
|
|
LastUpdated: mustParseTime("2024-01-15T10:30:00Z"),
|
|
StandbySkipped: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "entry with nil temperature",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: "/dev/sdb",
|
|
Temperature: nil,
|
|
},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{
|
|
Device: "/dev/sdb",
|
|
Temperature: 0, // nil becomes 0
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "entry with standby skipped",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: "/dev/sdc",
|
|
StandbySkipped: true,
|
|
Temperature: nil,
|
|
},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{
|
|
Device: "/dev/sdc",
|
|
StandbySkipped: true,
|
|
Temperature: 0,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty device skipped",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: "",
|
|
Temperature: intPtr(40),
|
|
},
|
|
},
|
|
want: []models.DiskTemp{},
|
|
},
|
|
{
|
|
name: "whitespace-only device skipped",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: " ",
|
|
Temperature: intPtr(40),
|
|
},
|
|
},
|
|
want: []models.DiskTemp{},
|
|
},
|
|
{
|
|
name: "invalid timestamp ignored",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: "/dev/sda",
|
|
LastUpdated: "not-a-timestamp",
|
|
Temperature: intPtr(42),
|
|
},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{
|
|
Device: "/dev/sda",
|
|
Temperature: 42,
|
|
LastUpdated: time.Time{}, // zero time
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "empty timestamp",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: "/dev/sda",
|
|
LastUpdated: "",
|
|
Temperature: intPtr(42),
|
|
},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{
|
|
Device: "/dev/sda",
|
|
Temperature: 42,
|
|
LastUpdated: time.Time{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "multiple entries",
|
|
raw: []smartEntryRaw{
|
|
{Device: "/dev/sda", Temperature: intPtr(38), Type: "sata"},
|
|
{Device: "/dev/sdb", Temperature: intPtr(40), Type: "sata"},
|
|
{Device: "/dev/nvme0n1", Temperature: intPtr(45), Type: "nvme"},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{Device: "/dev/sda", Temperature: 38, Type: "sata"},
|
|
{Device: "/dev/sdb", Temperature: 40, Type: "sata"},
|
|
{Device: "/dev/nvme0n1", Temperature: 45, Type: "nvme"},
|
|
},
|
|
},
|
|
{
|
|
name: "whitespace trimmed from fields",
|
|
raw: []smartEntryRaw{
|
|
{
|
|
Device: " /dev/sda ",
|
|
Serial: " ABC123 ",
|
|
WWN: " 1234 ",
|
|
Model: " Model X ",
|
|
Type: " sata ",
|
|
},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{
|
|
Device: "/dev/sda",
|
|
Serial: "ABC123",
|
|
WWN: "1234",
|
|
Model: "Model X",
|
|
Type: "sata",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "mixed valid and empty devices",
|
|
raw: []smartEntryRaw{
|
|
{Device: "/dev/sda", Temperature: intPtr(38)},
|
|
{Device: "", Temperature: intPtr(40)},
|
|
{Device: "/dev/sdc", Temperature: intPtr(42)},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{Device: "/dev/sda", Temperature: 38},
|
|
{Device: "/dev/sdc", Temperature: 42},
|
|
},
|
|
},
|
|
{
|
|
name: "zero temperature",
|
|
raw: []smartEntryRaw{
|
|
{Device: "/dev/sda", Temperature: intPtr(0)},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{Device: "/dev/sda", Temperature: 0},
|
|
},
|
|
},
|
|
{
|
|
name: "negative temperature",
|
|
raw: []smartEntryRaw{
|
|
{Device: "/dev/sda", Temperature: intPtr(-10)},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{Device: "/dev/sda", Temperature: -10},
|
|
},
|
|
},
|
|
{
|
|
name: "high temperature",
|
|
raw: []smartEntryRaw{
|
|
{Device: "/dev/sda", Temperature: intPtr(85)},
|
|
},
|
|
want: []models.DiskTemp{
|
|
{Device: "/dev/sda", Temperature: 85},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := normalizeSMARTEntries(tt.raw)
|
|
|
|
if tt.want == nil {
|
|
if got != nil {
|
|
t.Errorf("normalizeSMARTEntries() = %v, want nil", got)
|
|
}
|
|
return
|
|
}
|
|
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("normalizeSMARTEntries() returned %d entries, want %d", len(got), len(tt.want))
|
|
}
|
|
|
|
for i := range got {
|
|
if got[i].Device != tt.want[i].Device {
|
|
t.Errorf("entry[%d].Device = %q, want %q", i, got[i].Device, tt.want[i].Device)
|
|
}
|
|
if got[i].Serial != tt.want[i].Serial {
|
|
t.Errorf("entry[%d].Serial = %q, want %q", i, got[i].Serial, tt.want[i].Serial)
|
|
}
|
|
if got[i].WWN != tt.want[i].WWN {
|
|
t.Errorf("entry[%d].WWN = %q, want %q", i, got[i].WWN, tt.want[i].WWN)
|
|
}
|
|
if got[i].Model != tt.want[i].Model {
|
|
t.Errorf("entry[%d].Model = %q, want %q", i, got[i].Model, tt.want[i].Model)
|
|
}
|
|
if got[i].Type != tt.want[i].Type {
|
|
t.Errorf("entry[%d].Type = %q, want %q", i, got[i].Type, tt.want[i].Type)
|
|
}
|
|
if got[i].Temperature != tt.want[i].Temperature {
|
|
t.Errorf("entry[%d].Temperature = %d, want %d", i, got[i].Temperature, tt.want[i].Temperature)
|
|
}
|
|
if !got[i].LastUpdated.Equal(tt.want[i].LastUpdated) {
|
|
t.Errorf("entry[%d].LastUpdated = %v, want %v", i, got[i].LastUpdated, tt.want[i].LastUpdated)
|
|
}
|
|
if got[i].StandbySkipped != tt.want[i].StandbySkipped {
|
|
t.Errorf("entry[%d].StandbySkipped = %v, want %v", i, got[i].StandbySkipped, tt.want[i].StandbySkipped)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper functions for test setup
|
|
|
|
func intPtr(i int) *int {
|
|
return &i
|
|
}
|
|
|
|
func mustParseTime(s string) time.Time {
|
|
t, err := time.Parse(time.RFC3339, s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return t
|
|
}
|