drive-health-metrics/exporter_test.go
James Coleman ddafa90a02
Some checks failed
Go package / build (push) Has been cancelled
first commit
2026-06-22 17:16:34 -05:00

119 lines
3.6 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)
// sampleDrive returns a populated drive for exercising the output encoders.
func sampleDrive() *Drive {
d := &Drive{
Hostname: "kvm60",
Model: "Samsung SSD",
Serial: "S1",
Firmware: "1B6Q",
SmartHealth: "PASSED",
Enclosure: "64",
Slot: "3",
WearSrc: "nvme",
TempC: pInt(34),
PowerOnHours: pInt(17520),
PowerCycleCount: pInt(12),
WearPctConsumed: pInt(7),
HostWrittenTB: pF(12.5),
SmartAlertCtrl: true,
}
finalizeDerived(d)
return d
}
// The Prometheus collector must emit numeric gauges named with the namespace
// prefix, carrying the shared identity label set with consistent cardinality.
func TestDriveExporterCollect(t *testing.T) {
app = &App{config: defaultConfig()}
app.config.Hostname = "kvm60"
exp := NewDriveExporter()
// Inject a fixed drive set so Collect runs without touching real hardware.
exp.collect = func() ([]*Drive, int64) { return []*Drive{sampleDrive()}, 0 }
reg := prometheus.NewRegistry()
reg.MustRegister(exp)
mfs, err := reg.Gather()
if err != nil {
t.Fatalf("gather: %v", err)
}
byName := map[string]*dto.MetricFamily{}
for _, mf := range mfs {
byName[mf.GetName()] = mf
}
// A representative int, float, and bool field must be present and typed.
checks := map[string]float64{
"drive_health_temp_c": 34,
"drive_health_power_cycle_count": 12,
"drive_health_host_written_tb": 12.5,
"drive_health_smart_alert_ctrl": 1, // bool true -> 1
"drive_health_risk_score": float64(sampleDrive().RiskScore),
}
for name, want := range checks {
mf, ok := byName[name]
if !ok {
t.Errorf("missing metric %s", name)
continue
}
m := mf.GetMetric()[0]
if got := m.GetGauge().GetValue(); got != want {
t.Errorf("%s = %v, want %v", name, got, want)
}
// Identity labels must be attached.
labels := map[string]string{}
for _, l := range m.GetLabel() {
labels[l.GetName()] = l.GetValue()
}
if labels["serial"] != "S1" || labels["hostname"] != "kvm60" || labels["enclosure_slot"] != "64:3" {
t.Errorf("%s labels = %v", name, labels)
}
}
}
// The InfluxDB JSON encoder must produce one typed object per drive with tags,
// fields, and a microsecond timestamp.
func TestRecordsToInfluxJSON(t *testing.T) {
out := recordsToInfluxJSON([]*Drive{sampleDrive()}, 1700000000000000000)
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) != 1 {
t.Fatalf("got %d lines, want 1: %q", len(lines), out)
}
var obj struct {
Name string `json:"name"`
Tags map[string]string `json:"tags"`
Fields map[string]interface{} `json:"fields"`
Timestamp int64 `json:"timestamp"`
}
if err := json.Unmarshal([]byte(lines[0]), &obj); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if obj.Name != influxMeasurement {
t.Errorf("name = %q, want %q", obj.Name, influxMeasurement)
}
if obj.Tags["serial"] != "S1" || obj.Tags["model"] != "Samsung SSD" {
t.Errorf("tags = %v", obj.Tags)
}
if obj.Timestamp != 1700000000000000 {
t.Errorf("timestamp = %d, want microseconds", obj.Timestamp)
}
// int field decodes as a JSON number; bool field as a real bool.
if v, ok := obj.Fields["temp_c"].(float64); !ok || v != 34 {
t.Errorf("temp_c field = %v", obj.Fields["temp_c"])
}
if v, ok := obj.Fields["smart_alert_ctrl"].(bool); !ok || !v {
t.Errorf("smart_alert_ctrl field = %v", obj.Fields["smart_alert_ctrl"])
}
}