mirror of
https://github.com/fankes/beszel.git
synced 2025-10-18 17:29:28 +08:00
555 lines
16 KiB
Go
555 lines
16 KiB
Go
//go:build testing
|
|
// +build testing
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/henrygd/beszel/internal/entities/system"
|
|
|
|
"github.com/shirou/gopsutil/v4/common"
|
|
"github.com/shirou/gopsutil/v4/sensors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestIsValidSensor(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sensorName string
|
|
config *SensorConfig
|
|
expectedValid bool
|
|
}{
|
|
{
|
|
name: "Whitelist - sensor in list",
|
|
sensorName: "cpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}},
|
|
isBlacklist: false,
|
|
},
|
|
expectedValid: true,
|
|
},
|
|
{
|
|
name: "Whitelist - sensor not in list",
|
|
sensorName: "gpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}},
|
|
isBlacklist: false,
|
|
},
|
|
expectedValid: false,
|
|
},
|
|
{
|
|
name: "Blacklist - sensor in list",
|
|
sensorName: "cpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}},
|
|
isBlacklist: true,
|
|
},
|
|
expectedValid: false,
|
|
},
|
|
{
|
|
name: "Blacklist - sensor not in list",
|
|
sensorName: "gpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}},
|
|
isBlacklist: true,
|
|
},
|
|
expectedValid: true,
|
|
},
|
|
{
|
|
name: "Whitelist with wildcard - matching pattern",
|
|
sensorName: "core_0_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"core_*_temp": {}},
|
|
isBlacklist: false,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: true,
|
|
},
|
|
{
|
|
name: "Whitelist with wildcard - non-matching pattern",
|
|
sensorName: "gpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"core_*_temp": {}},
|
|
isBlacklist: false,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: false,
|
|
},
|
|
{
|
|
name: "Blacklist with wildcard - matching pattern",
|
|
sensorName: "core_0_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"core_*_temp": {}},
|
|
isBlacklist: true,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: false,
|
|
},
|
|
{
|
|
name: "Blacklist with wildcard - non-matching pattern",
|
|
sensorName: "gpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"core_*_temp": {}},
|
|
isBlacklist: true,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: true,
|
|
},
|
|
{
|
|
name: "No sensors configured",
|
|
sensorName: "any_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{},
|
|
isBlacklist: false,
|
|
hasWildcards: false,
|
|
skipCollection: false,
|
|
},
|
|
expectedValid: true,
|
|
},
|
|
{
|
|
name: "Mixed patterns in whitelist - exact match",
|
|
sensorName: "cpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
|
isBlacklist: false,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: true,
|
|
},
|
|
{
|
|
name: "Mixed patterns in whitelist - wildcard match",
|
|
sensorName: "core_1_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
|
isBlacklist: false,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: true,
|
|
},
|
|
{
|
|
name: "Mixed patterns in blacklist - exact match",
|
|
sensorName: "cpu_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
|
isBlacklist: true,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: false,
|
|
},
|
|
{
|
|
name: "Mixed patterns in blacklist - wildcard match",
|
|
sensorName: "core_1_temp",
|
|
config: &SensorConfig{
|
|
sensors: map[string]struct{}{"cpu_temp": {}, "core_*_temp": {}},
|
|
isBlacklist: true,
|
|
hasWildcards: true,
|
|
},
|
|
expectedValid: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := isValidSensor(tt.sensorName, tt.config)
|
|
assert.Equal(t, tt.expectedValid, result, "isValidSensor(%q, config) returned unexpected result", tt.sensorName)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSensorConfigWithEnv(t *testing.T) {
|
|
agent := &Agent{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
primarySensor string
|
|
sysSensors string
|
|
sensors string
|
|
skipCollection bool
|
|
expectedConfig *SensorConfig
|
|
}{
|
|
{
|
|
name: "Empty configuration",
|
|
primarySensor: "",
|
|
sysSensors: "",
|
|
sensors: "",
|
|
expectedConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
primarySensor: "",
|
|
sensors: map[string]struct{}{},
|
|
isBlacklist: false,
|
|
hasWildcards: false,
|
|
skipCollection: false,
|
|
},
|
|
},
|
|
{
|
|
name: "Explicitly set to empty string",
|
|
primarySensor: "",
|
|
sysSensors: "",
|
|
sensors: "",
|
|
skipCollection: true,
|
|
expectedConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
primarySensor: "",
|
|
sensors: map[string]struct{}{},
|
|
isBlacklist: false,
|
|
hasWildcards: false,
|
|
skipCollection: true,
|
|
},
|
|
},
|
|
{
|
|
name: "Primary sensor only - should create sensor map",
|
|
primarySensor: "cpu_temp",
|
|
sysSensors: "",
|
|
sensors: "",
|
|
expectedConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
primarySensor: "cpu_temp",
|
|
sensors: map[string]struct{}{},
|
|
isBlacklist: false,
|
|
hasWildcards: false,
|
|
},
|
|
},
|
|
{
|
|
name: "Whitelist sensors",
|
|
primarySensor: "cpu_temp",
|
|
sysSensors: "",
|
|
sensors: "cpu_temp,gpu_temp",
|
|
expectedConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
primarySensor: "cpu_temp",
|
|
sensors: map[string]struct{}{
|
|
"cpu_temp": {},
|
|
"gpu_temp": {},
|
|
},
|
|
isBlacklist: false,
|
|
hasWildcards: false,
|
|
},
|
|
},
|
|
{
|
|
name: "Blacklist sensors",
|
|
primarySensor: "cpu_temp",
|
|
sysSensors: "",
|
|
sensors: "-cpu_temp,gpu_temp",
|
|
expectedConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
primarySensor: "cpu_temp",
|
|
sensors: map[string]struct{}{
|
|
"cpu_temp": {},
|
|
"gpu_temp": {},
|
|
},
|
|
isBlacklist: true,
|
|
hasWildcards: false,
|
|
},
|
|
},
|
|
{
|
|
name: "Sensors with wildcard",
|
|
primarySensor: "cpu_temp",
|
|
sysSensors: "",
|
|
sensors: "cpu_*,gpu_temp",
|
|
expectedConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
primarySensor: "cpu_temp",
|
|
sensors: map[string]struct{}{
|
|
"cpu_*": {},
|
|
"gpu_temp": {},
|
|
},
|
|
isBlacklist: false,
|
|
hasWildcards: true,
|
|
},
|
|
},
|
|
{
|
|
name: "Sensors with whitespace",
|
|
primarySensor: "cpu_temp",
|
|
sysSensors: "",
|
|
sensors: "cpu_*, gpu_temp",
|
|
expectedConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
primarySensor: "cpu_temp",
|
|
sensors: map[string]struct{}{
|
|
"cpu_*": {},
|
|
"gpu_temp": {},
|
|
},
|
|
isBlacklist: false,
|
|
hasWildcards: true,
|
|
},
|
|
},
|
|
{
|
|
name: "With SYS_SENSORS path",
|
|
primarySensor: "cpu_temp",
|
|
sysSensors: "/custom/path",
|
|
sensors: "cpu_temp",
|
|
expectedConfig: &SensorConfig{
|
|
primarySensor: "cpu_temp",
|
|
sensors: map[string]struct{}{
|
|
"cpu_temp": {},
|
|
},
|
|
isBlacklist: false,
|
|
hasWildcards: false,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := agent.newSensorConfigWithEnv(tt.primarySensor, tt.sysSensors, tt.sensors, tt.skipCollection)
|
|
|
|
// Check primary sensor
|
|
assert.Equal(t, tt.expectedConfig.primarySensor, result.primarySensor)
|
|
|
|
// Check sensor map
|
|
if tt.expectedConfig.sensors == nil {
|
|
assert.Nil(t, result.sensors)
|
|
} else {
|
|
assert.Equal(t, len(tt.expectedConfig.sensors), len(result.sensors))
|
|
for sensor := range tt.expectedConfig.sensors {
|
|
_, exists := result.sensors[sensor]
|
|
assert.True(t, exists, "Sensor %s should exist in the result", sensor)
|
|
}
|
|
}
|
|
|
|
// Check flags
|
|
assert.Equal(t, tt.expectedConfig.isBlacklist, result.isBlacklist)
|
|
assert.Equal(t, tt.expectedConfig.hasWildcards, result.hasWildcards)
|
|
|
|
// Check context
|
|
if tt.sysSensors != "" {
|
|
// Verify context contains correct values
|
|
envMap, ok := result.context.Value(common.EnvKey).(common.EnvMap)
|
|
require.True(t, ok, "Context should contain EnvMap")
|
|
sysPath, ok := envMap[common.HostSysEnvKey]
|
|
require.True(t, ok, "EnvMap should contain HostSysEnvKey")
|
|
assert.Equal(t, tt.sysSensors, sysPath)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewSensorConfig(t *testing.T) {
|
|
// Save original environment variables
|
|
originalPrimary, hasPrimary := os.LookupEnv("BESZEL_AGENT_PRIMARY_SENSOR")
|
|
originalSys, hasSys := os.LookupEnv("BESZEL_AGENT_SYS_SENSORS")
|
|
originalSensors, hasSensors := os.LookupEnv("BESZEL_AGENT_SENSORS")
|
|
|
|
// Restore environment variables after the test
|
|
defer func() {
|
|
// Clean up test environment variables
|
|
os.Unsetenv("BESZEL_AGENT_PRIMARY_SENSOR")
|
|
os.Unsetenv("BESZEL_AGENT_SYS_SENSORS")
|
|
os.Unsetenv("BESZEL_AGENT_SENSORS")
|
|
|
|
// Restore original values if they existed
|
|
if hasPrimary {
|
|
os.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", originalPrimary)
|
|
}
|
|
if hasSys {
|
|
os.Setenv("BESZEL_AGENT_SYS_SENSORS", originalSys)
|
|
}
|
|
if hasSensors {
|
|
os.Setenv("BESZEL_AGENT_SENSORS", originalSensors)
|
|
}
|
|
}()
|
|
|
|
// Set test environment variables
|
|
os.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary")
|
|
os.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path")
|
|
os.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3")
|
|
|
|
agent := &Agent{}
|
|
result := agent.newSensorConfig()
|
|
|
|
// Verify results
|
|
assert.Equal(t, "test_primary", result.primarySensor)
|
|
assert.NotNil(t, result.sensors)
|
|
assert.Equal(t, 3, len(result.sensors))
|
|
assert.True(t, result.hasWildcards)
|
|
assert.False(t, result.isBlacklist)
|
|
|
|
// Check that sys sensors path is in context
|
|
envMap, ok := result.context.Value(common.EnvKey).(common.EnvMap)
|
|
require.True(t, ok, "Context should contain EnvMap")
|
|
sysPath, ok := envMap[common.HostSysEnvKey]
|
|
require.True(t, ok, "EnvMap should contain HostSysEnvKey")
|
|
assert.Equal(t, "/test/path", sysPath)
|
|
}
|
|
|
|
func TestScaleTemperature(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input float64
|
|
expected float64
|
|
desc string
|
|
}{
|
|
// Normal temperatures (no scaling needed)
|
|
{"normal_cpu_temp", 45.0, 45.0, "Normal CPU temperature"},
|
|
{"normal_room_temp", 25.0, 25.0, "Normal room temperature"},
|
|
{"high_cpu_temp", 85.0, 85.0, "High CPU temperature"},
|
|
// Zero temperature
|
|
{"zero_temp", 0.0, 0.0, "Zero temperature"},
|
|
// Fractional values that should use 100x scaling
|
|
{"fractional_45c", 0.45, 45.0, "0.45 should become 45°C (100x)"},
|
|
{"fractional_25c", 0.25, 25.0, "0.25 should become 25°C (100x)"},
|
|
{"fractional_60c", 0.60, 60.0, "0.60 should become 60°C (100x)"},
|
|
{"fractional_75c", 0.75, 75.0, "0.75 should become 75°C (100x)"},
|
|
{"fractional_30c", 0.30, 30.0, "0.30 should become 30°C (100x)"},
|
|
// Fractional values that should use 1000x scaling
|
|
{"millifractional_45c", 0.045, 45.0, "0.045 should become 45°C (1000x)"},
|
|
{"millifractional_25c", 0.025, 25.0, "0.025 should become 25°C (1000x)"},
|
|
{"millifractional_60c", 0.060, 60.0, "0.060 should become 60°C (1000x)"},
|
|
{"millifractional_75c", 0.075, 75.0, "0.075 should become 75°C (1000x)"},
|
|
{"millifractional_35c", 0.035, 35.0, "0.035 should become 35°C (1000x)"},
|
|
// Edge cases - values outside reasonable range
|
|
{"very_low_fractional", 0.01, 1.0, "0.01 should default to 100x scaling (1°C)"},
|
|
{"very_high_fractional", 0.99, 99.0, "0.99 should default to 100x scaling (99°C)"},
|
|
{"extremely_low", 0.001, 0.1, "0.001 should default to 100x scaling (0.1°C)"},
|
|
// Boundary cases around the reasonable range (15-95°C)
|
|
{"boundary_low_100x", 0.15, 15.0, "0.15 should use 100x scaling (15°C)"},
|
|
{"boundary_high_100x", 0.95, 95.0, "0.95 should use 100x scaling (95°C)"},
|
|
{"boundary_low_1000x", 0.015, 15.0, "0.015 should use 1000x scaling (15°C)"},
|
|
{"boundary_high_1000x", 0.095, 95.0, "0.095 should use 1000x scaling (95°C)"},
|
|
// Values just outside reasonable range
|
|
{"just_below_range_100x", 0.14, 14.0, "0.14 should default to 100x (14°C)"},
|
|
{"just_above_range_100x", 0.96, 96.0, "0.96 should default to 100x (96°C)"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := scaleTemperature(tt.input)
|
|
assert.InDelta(t, tt.expected, result, 0.001,
|
|
"scaleTemperature(%v) = %v, expected %v (%s)",
|
|
tt.input, result, tt.expected, tt.desc)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestScaleTemperatureLogic(t *testing.T) {
|
|
// Test the logic flow for ambiguous cases
|
|
t.Run("prefers_100x_when_both_valid", func(t *testing.T) {
|
|
// 0.5 could be 50°C (100x) or 500°C (1000x)
|
|
// Should prefer 100x since it's tried first and is in range
|
|
result := scaleTemperature(0.5)
|
|
expected := 50.0
|
|
assert.InDelta(t, expected, result, 0.001,
|
|
"scaleTemperature(0.5) = %v, expected %v (should prefer 100x scaling)",
|
|
result, expected)
|
|
})
|
|
|
|
t.Run("uses_1000x_when_100x_too_low", func(t *testing.T) {
|
|
// 0.05 -> 5°C (100x, too low) or 50°C (1000x, in range)
|
|
// Should use 1000x since 100x is below reasonable range
|
|
result := scaleTemperature(0.05)
|
|
expected := 50.0
|
|
assert.InDelta(t, expected, result, 0.001,
|
|
"scaleTemperature(0.05) = %v, expected %v (should use 1000x scaling)",
|
|
result, expected)
|
|
})
|
|
|
|
t.Run("defaults_to_100x_when_both_invalid", func(t *testing.T) {
|
|
// 0.005 -> 0.5°C (100x, too low) or 5°C (1000x, too low)
|
|
// Should default to 100x scaling
|
|
result := scaleTemperature(0.005)
|
|
expected := 0.5
|
|
assert.InDelta(t, expected, result, 0.001,
|
|
"scaleTemperature(0.005) = %v, expected %v (should default to 100x)",
|
|
result, expected)
|
|
})
|
|
}
|
|
|
|
func TestGetTempsWithPanicRecovery(t *testing.T) {
|
|
agent := &Agent{
|
|
systemInfo: system.Info{},
|
|
sensorConfig: &SensorConfig{
|
|
context: context.Background(),
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
getTempsFn getTempsFn
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "successful_function_call",
|
|
getTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {
|
|
return []sensors.TemperatureStat{
|
|
{SensorKey: "test_sensor", Temperature: 45.0},
|
|
}, nil
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "function_returns_error",
|
|
getTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {
|
|
return []sensors.TemperatureStat{
|
|
{SensorKey: "test_sensor", Temperature: 45.0},
|
|
}, fmt.Errorf("sensor error")
|
|
},
|
|
expectError: false, // getTempsWithPanicRecovery ignores errors from the function
|
|
},
|
|
{
|
|
name: "function_panics_with_string",
|
|
getTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {
|
|
panic("test panic")
|
|
},
|
|
expectError: true,
|
|
errorMsg: "panic: test panic",
|
|
},
|
|
{
|
|
name: "function_panics_with_error",
|
|
getTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {
|
|
panic(fmt.Errorf("panic error"))
|
|
},
|
|
expectError: true,
|
|
errorMsg: "panic:",
|
|
},
|
|
{
|
|
name: "function_panics_with_index_out_of_bounds",
|
|
getTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {
|
|
slice := []int{1, 2, 3}
|
|
_ = slice[10] // out of bounds panic
|
|
return nil, nil
|
|
},
|
|
expectError: true,
|
|
errorMsg: "panic:",
|
|
},
|
|
{
|
|
name: "function_panics_with_any_conversion",
|
|
getTempsFn: func(ctx context.Context) ([]sensors.TemperatureStat, error) {
|
|
var i any = "string"
|
|
_ = i.(int) // type assertion panic
|
|
return nil, nil
|
|
},
|
|
expectError: true,
|
|
errorMsg: "panic:",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var temps []sensors.TemperatureStat
|
|
var err error
|
|
|
|
// The function should not panic, regardless of what the injected function does
|
|
assert.NotPanics(t, func() {
|
|
temps, err = agent.getTempsWithPanicRecovery(tt.getTempsFn)
|
|
}, "getTempsWithPanicRecovery should not panic")
|
|
|
|
if tt.expectError {
|
|
assert.Error(t, err, "Expected an error to be returned")
|
|
if tt.errorMsg != "" {
|
|
assert.Contains(t, err.Error(), tt.errorMsg,
|
|
"Error message should contain expected text")
|
|
}
|
|
assert.Nil(t, temps, "Temps should be nil when panic occurs")
|
|
} else {
|
|
assert.NoError(t, err, "Should not return error for successful calls")
|
|
}
|
|
})
|
|
}
|
|
}
|