119 lines
3.6 KiB
Go
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"])
|
|
}
|
|
}
|