test: cover IPMI, PSU, and SAT formatting
This commit is contained in:
@@ -30,3 +30,41 @@ func TestFormatGPULineIgnoresNonGPUSameVendorDevice(t *testing.T) {
|
|||||||
t.Fatalf("formatGPULine() = %q, want %q", got, want)
|
t.Fatalf("formatGPULine() = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormatSATDetail(t *testing.T) {
|
||||||
|
raw := `run_at_utc=2026-09-12T16:30:00Z
|
||||||
|
01-cpu_status=OK
|
||||||
|
storage_status=FAILED
|
||||||
|
tpm_status=UNSUPPORTED
|
||||||
|
overall_status=FAILED
|
||||||
|
job_ok=1
|
||||||
|
job_failed=1`
|
||||||
|
|
||||||
|
got := formatSATDetail(raw)
|
||||||
|
want := `Run: 2026-09-12T16:30:00Z
|
||||||
|
|
||||||
|
PASS cpu
|
||||||
|
FAIL storage
|
||||||
|
SKIP tpm
|
||||||
|
|
||||||
|
Overall: FAILED (ok=1 failed=1)`
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("formatSATDetail() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanSummaryKey(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{input: "01-cpu", want: "cpu"},
|
||||||
|
{input: "cpu", want: "cpu"},
|
||||||
|
{input: "cpu-01", want: "cpu-01"},
|
||||||
|
{input: "-cpu", want: "-cpu"},
|
||||||
|
} {
|
||||||
|
if got := cleanSummaryKey(tt.input); got != tt.want {
|
||||||
|
t.Errorf("cleanSummaryKey(%q) = %q, want %q", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,30 @@ func TestParseBIOSFirmware_empty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseBMCFirmwareRevision(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
out string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "value with surrounding whitespace",
|
||||||
|
out: "Device ID : 32\nFirmware Revision : 2.14\n",
|
||||||
|
want: "2.14",
|
||||||
|
},
|
||||||
|
{name: "field missing", out: "Device ID : 32\n", want: ""},
|
||||||
|
{name: "malformed line", out: "Firmware Revision 2.14\n", want: ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := parseBMCFirmwareRevision(tt.out); got != tt.want {
|
||||||
|
t.Fatalf("parseBMCFirmwareRevision() = %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCleanDMIValue(t *testing.T) {
|
func TestCleanDMIValue(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input string
|
input string
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bee/audit/internal/schema"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseIPMISELOutput(t *testing.T) {
|
||||||
|
entries := parseIPMISELOutput(`
|
||||||
|
1 | 06/18/2026 | 14:23:45 | Temperature #0x30 | Upper Critical going high | Asserted
|
||||||
|
2 | 06/18/2026 | 14:24:45 | Voltage #0x21 | Upper Non-critical going high | Deasserted
|
||||||
|
malformed
|
||||||
|
`)
|
||||||
|
if len(entries) != 2 {
|
||||||
|
t.Fatalf("entries = %d, want 2", len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
first := entries[0]
|
||||||
|
if first.Source != "ipmi-sel" || first.MessageID == nil || *first.MessageID != "1" {
|
||||||
|
t.Fatalf("first entry identity = %#v", first)
|
||||||
|
}
|
||||||
|
if first.EventTime == nil || *first.EventTime != "06/18/2026 14:23:45" {
|
||||||
|
t.Fatalf("first event time = %v", first.EventTime)
|
||||||
|
}
|
||||||
|
if first.ComponentRef == nil || *first.ComponentRef != "Temperature #0x30" {
|
||||||
|
t.Fatalf("first component = %v", first.ComponentRef)
|
||||||
|
}
|
||||||
|
if first.Severity == nil || *first.Severity != statusCritical || first.IsActive == nil || !*first.IsActive {
|
||||||
|
t.Fatalf("first severity/activity = %#v", first)
|
||||||
|
}
|
||||||
|
|
||||||
|
second := entries[1]
|
||||||
|
if second.Message != "Upper Non-critical going high (Deasserted)" {
|
||||||
|
t.Fatalf("second message = %q", second.Message)
|
||||||
|
}
|
||||||
|
if second.Severity == nil || *second.Severity != statusWarning || second.IsActive == nil || *second.IsActive {
|
||||||
|
t.Fatalf("second severity/activity = %#v", second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPMISELSeverity(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
event string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{event: "Upper Critical going high", want: statusCritical},
|
||||||
|
{event: "Non-recoverable fault", want: statusCritical},
|
||||||
|
{event: "Upper Non-critical going high", want: statusWarning},
|
||||||
|
{event: "Voltage degraded", want: statusWarning},
|
||||||
|
{event: "Device inserted", want: "info"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := ipmiSELSeverity(tt.event); got != tt.want {
|
||||||
|
t.Errorf("ipmiSELSeverity(%q) = %q, want %q", tt.event, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseIPMISensorOutput(t *testing.T) {
|
||||||
|
sensors := parseIPMISensorOutput(`
|
||||||
|
FAN1 | 4200 | RPM | ok | na | na | na | na | na | na
|
||||||
|
CPU Temp | 92.5 | degrees C | ns | na | na | na | 80 | 90 | na
|
||||||
|
CPU Temp | 80 | degrees C | ok | na | na | na | 80 | 90 | na
|
||||||
|
PSU Input | 12.1 | Volts | ok | na | na | na | na | na | na
|
||||||
|
System Power | 650 | Watts | cr | na | na | na | na | na | na
|
||||||
|
Ambient | 44 | percent | nc | na | na | na | na | na | na
|
||||||
|
Missing | na | RPM | ok
|
||||||
|
Broken | not-a-number | RPM | ok
|
||||||
|
`)
|
||||||
|
if sensors == nil {
|
||||||
|
t.Fatal("expected sensors")
|
||||||
|
}
|
||||||
|
if len(sensors.Fans) != 1 || sensors.Fans[0].RPM == nil || *sensors.Fans[0].RPM != 4200 {
|
||||||
|
t.Fatalf("fans = %#v", sensors.Fans)
|
||||||
|
}
|
||||||
|
if len(sensors.Temperatures) != 1 {
|
||||||
|
t.Fatalf("temperatures = %#v", sensors.Temperatures)
|
||||||
|
}
|
||||||
|
temp := sensors.Temperatures[0]
|
||||||
|
if temp.ThresholdWarningCelsius == nil || *temp.ThresholdWarningCelsius != 80 ||
|
||||||
|
temp.ThresholdCriticalCelsius == nil || *temp.ThresholdCriticalCelsius != 90 ||
|
||||||
|
temp.Status == nil || *temp.Status != statusCritical {
|
||||||
|
t.Fatalf("temperature = %#v", temp)
|
||||||
|
}
|
||||||
|
if len(sensors.Power) != 2 || sensors.Power[0].VoltageV == nil || sensors.Power[1].PowerW == nil ||
|
||||||
|
sensors.Power[1].Status == nil || *sensors.Power[1].Status != statusCritical {
|
||||||
|
t.Fatalf("power = %#v", sensors.Power)
|
||||||
|
}
|
||||||
|
if len(sensors.Other) != 1 || sensors.Other[0].Unit == nil || *sensors.Other[0].Unit != "percent" ||
|
||||||
|
sensors.Other[0].Status == nil || *sensors.Other[0].Status != statusWarning {
|
||||||
|
t.Fatalf("other = %#v", sensors.Other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseIPMIThresholdAndStatus(t *testing.T) {
|
||||||
|
if got := parseIPMIThreshold(" 42.5 "); got == nil || *got != 42.5 {
|
||||||
|
t.Fatalf("threshold = %v, want 42.5", got)
|
||||||
|
}
|
||||||
|
for _, raw := range []string{"", "na", "N/A", "not-a-number"} {
|
||||||
|
if got := parseIPMIThreshold(raw); got != nil {
|
||||||
|
t.Errorf("parseIPMIThreshold(%q) = %v, want nil", raw, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for raw, want := range map[string]string{
|
||||||
|
"ok": statusOK, "cr": statusCritical, "unc": statusWarning, "na": "", "other": "",
|
||||||
|
} {
|
||||||
|
if got := normalizeIPMISensorStatus(raw); got != want {
|
||||||
|
t.Errorf("normalizeIPMISensorStatus(%q) = %q, want %q", raw, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMergeIPMISensors(t *testing.T) {
|
||||||
|
existing := &schema.HardwareSensors{
|
||||||
|
Fans: []schema.HardwareFanSensor{{Name: "FAN1"}},
|
||||||
|
}
|
||||||
|
ipmi := &schema.HardwareSensors{
|
||||||
|
Fans: []schema.HardwareFanSensor{{Name: "FAN1"}, {Name: "FAN2"}},
|
||||||
|
Temperatures: []schema.HardwareTemperatureSensor{{Name: "CPU Temp"}},
|
||||||
|
}
|
||||||
|
merged := mergeIPMISensors(existing, ipmi)
|
||||||
|
if merged != existing || len(merged.Fans) != 2 || len(merged.Temperatures) != 1 {
|
||||||
|
t.Fatalf("merged = %#v", merged)
|
||||||
|
}
|
||||||
|
if got := mergeIPMISensors(existing, nil); got != existing {
|
||||||
|
t.Fatal("nil IPMI sensors should preserve existing sensors")
|
||||||
|
}
|
||||||
|
if got := mergeIPMISensors(nil, ipmi); got != ipmi {
|
||||||
|
t.Fatal("nil existing sensors should return IPMI sensors")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,10 +80,10 @@ func parseIPMISELOutput(output string) []schema.HardwareEventLog {
|
|||||||
func ipmiSELSeverity(event string) string {
|
func ipmiSELSeverity(event string) string {
|
||||||
lower := strings.ToLower(event)
|
lower := strings.ToLower(event)
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(lower, "critical") || strings.Contains(lower, "non-recoverable"):
|
|
||||||
return statusCritical
|
|
||||||
case strings.Contains(lower, "non-critical") || strings.Contains(lower, "warning") || strings.Contains(lower, "degraded"):
|
case strings.Contains(lower, "non-critical") || strings.Contains(lower, "warning") || strings.Contains(lower, "degraded"):
|
||||||
return statusWarning
|
return statusWarning
|
||||||
|
case strings.Contains(lower, "critical") || strings.Contains(lower, "non-recoverable"):
|
||||||
|
return statusCritical
|
||||||
default:
|
default:
|
||||||
return "info"
|
return "info"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,3 +133,46 @@ func TestMergePSUSDRAppendsPSUMissingFromFRU(t *testing.T) {
|
|||||||
t.Fatalf("PSUs=%#v, want FRU PSU0 plus synthesized PSU1", got)
|
t.Fatalf("PSUs=%#v, want FRU PSU0 plus synthesized PSU1", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseFRUFiltersNonPSUAndNormalizesSlots(t *testing.T) {
|
||||||
|
psus := parseFRU(`
|
||||||
|
FRU Device Description : Builtin FRU Device (ID 0)
|
||||||
|
Board Mfg : Example Systems
|
||||||
|
FRU Device Description : PSU 2 (ID 12)
|
||||||
|
Board Product : Server PSU 1600W
|
||||||
|
Board Mfg : Acme Power
|
||||||
|
Board Serial : PS2-SERIAL
|
||||||
|
Board Part Number : ACME-1600
|
||||||
|
Board Extra : 1.4
|
||||||
|
FRU Device Description : Power Supply Bay 3 (ID 13)
|
||||||
|
Product Name : Redundant 800W
|
||||||
|
Product Manufacturer : Acme Power
|
||||||
|
Product Serial : PS3-SERIAL
|
||||||
|
`)
|
||||||
|
if len(psus) != 2 {
|
||||||
|
t.Fatalf("PSUs = %#v, want two PSU FRUs", psus)
|
||||||
|
}
|
||||||
|
if psus[0].Slot == nil || *psus[0].Slot != "1" || psus[0].Model == nil || *psus[0].Model != "Server PSU 1600W" ||
|
||||||
|
psus[0].WattageW == nil || *psus[0].WattageW != 1600 || psus[0].Status == nil || *psus[0].Status != statusOK {
|
||||||
|
t.Fatalf("first PSU = %#v", psus[0])
|
||||||
|
}
|
||||||
|
if psus[1].Slot == nil || *psus[1].Slot != "2" || psus[1].SerialNumber == nil || *psus[1].SerialNumber != "PS3-SERIAL" {
|
||||||
|
t.Fatalf("second PSU = %#v", psus[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPSUNumericParsersRejectInvalidReadings(t *testing.T) {
|
||||||
|
for _, raw := range []string{"", "na", "0", "-1", "6001", "not-a-number"} {
|
||||||
|
if got := parseBoundedFloat(raw, 6000); got != nil {
|
||||||
|
t.Errorf("parseBoundedFloat(%q) = %v, want nil", raw, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := parseBoundedFloat("1200 Watts", 6000); got == nil || *got != 1200 {
|
||||||
|
t.Fatalf("parseBoundedFloat valid value = %v", got)
|
||||||
|
}
|
||||||
|
for raw, want := range map[string]int{"PSU 800W": 800, "1200W PLATINUM": 1200, "no wattage": 0, "6000W": 0} {
|
||||||
|
if got := parseWattage(raw); got != want {
|
||||||
|
t.Errorf("parseWattage(%q) = %d, want %d", raw, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package collector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bee/audit/internal/schema"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildHealthSummary(t *testing.T) {
|
||||||
|
warning := statusWarning
|
||||||
|
critical := statusCritical
|
||||||
|
empty := statusEmpty
|
||||||
|
present := true
|
||||||
|
absent := false
|
||||||
|
diskModel := "NVMe A"
|
||||||
|
pcieSerial := "GPU-1"
|
||||||
|
psuSlot := "PSU1"
|
||||||
|
dimmSlot := "DIMM_A1"
|
||||||
|
|
||||||
|
summary := BuildHealthSummary(schema.HardwareSnapshot{
|
||||||
|
Memory: []schema.HardwareMemory{
|
||||||
|
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &warning}, Slot: &dimmSlot},
|
||||||
|
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &empty}},
|
||||||
|
},
|
||||||
|
Storage: []schema.HardwareStorage{{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &critical}, Model: &diskModel}},
|
||||||
|
PCIeDevices: []schema.HardwarePCIeDevice{{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &warning}, SerialNumber: &pcieSerial}},
|
||||||
|
PowerSupplies: []schema.HardwarePowerSupply{
|
||||||
|
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &critical}, Slot: &psuSlot, Present: &present},
|
||||||
|
{Present: &absent},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if summary.Status != statusCritical || summary.MemoryWarn != 1 || summary.EmptyDIMMs != 1 ||
|
||||||
|
summary.StorageFail != 1 || summary.PCIeWarn != 1 || summary.PSUFail != 1 || summary.MissingPSUs != 1 {
|
||||||
|
t.Fatalf("summary = %#v", summary)
|
||||||
|
}
|
||||||
|
if len(summary.Warnings) != 2 || len(summary.Failures) != 2 {
|
||||||
|
t.Fatalf("warnings/failures = %#v", summary)
|
||||||
|
}
|
||||||
|
if _, err := time.Parse(time.RFC3339, summary.CollectedAt); err != nil {
|
||||||
|
t.Fatalf("collected_at = %q: %v", summary.CollectedAt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildHealthSummaryEmptySnapshot(t *testing.T) {
|
||||||
|
summary := BuildHealthSummary(schema.HardwareSnapshot{})
|
||||||
|
if summary.Status != statusOK || summary.Warnings != nil || summary.Failures != nil {
|
||||||
|
t.Fatalf("summary = %#v", summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreferredName(t *testing.T) {
|
||||||
|
model, serial, slot := "model", "serial", "slot"
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
model *string
|
||||||
|
serial *string
|
||||||
|
slot *string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "model", model: &model, serial: &serial, slot: &slot, want: "model"},
|
||||||
|
{name: "serial", serial: &serial, slot: &slot, want: "serial"},
|
||||||
|
{name: "slot", slot: &slot, want: "slot"},
|
||||||
|
{name: "fallback", want: "unknown"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := preferredName(tt.model, tt.serial, tt.slot); got != tt.want {
|
||||||
|
t.Errorf("%s: preferredName() = %q, want %q", tt.name, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user