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"]) } }