|
| 1 | +//go:build unittest |
| 2 | + |
| 3 | +package common |
| 4 | + |
| 5 | +import ( |
| 6 | + "reflect" |
| 7 | + "strings" |
| 8 | + "sync" |
| 9 | + "testing" |
| 10 | + |
| 11 | + "github.com/prometheus/client_golang/prometheus" |
| 12 | + "github.com/trezor/blockbook/configs" |
| 13 | + yaml "gopkg.in/yaml.v3" |
| 14 | +) |
| 15 | + |
| 16 | +var prometheusRegistryMu sync.Mutex |
| 17 | + |
| 18 | +func useTestPrometheusRegistry(t *testing.T) { |
| 19 | + t.Helper() |
| 20 | + |
| 21 | + prometheusRegistryMu.Lock() |
| 22 | + oldRegisterer := prometheus.DefaultRegisterer |
| 23 | + oldGatherer := prometheus.DefaultGatherer |
| 24 | + registry := prometheus.NewRegistry() |
| 25 | + prometheus.DefaultRegisterer = registry |
| 26 | + prometheus.DefaultGatherer = registry |
| 27 | + |
| 28 | + t.Cleanup(func() { |
| 29 | + prometheus.DefaultRegisterer = oldRegisterer |
| 30 | + prometheus.DefaultGatherer = oldGatherer |
| 31 | + prometheusRegistryMu.Unlock() |
| 32 | + }) |
| 33 | +} |
| 34 | + |
| 35 | +// TestGetMetrics verifies that every field of the Metrics struct is bound to a |
| 36 | +// definition in configs/metrics.yaml, of a matching type, and that the resulting |
| 37 | +// collectors are all constructed (non-nil) after loading. |
| 38 | +func TestGetMetrics(t *testing.T) { |
| 39 | + useTestPrometheusRegistry(t) |
| 40 | + |
| 41 | + m, err := GetMetrics("metrics_unittest") |
| 42 | + if err != nil { |
| 43 | + t.Fatalf("GetMetrics: %v", err) |
| 44 | + } |
| 45 | + v := reflect.ValueOf(m).Elem() |
| 46 | + tp := v.Type() |
| 47 | + for i := 0; i < tp.NumField(); i++ { |
| 48 | + if v.Field(i).IsNil() { |
| 49 | + t.Errorf("field %s was not initialized from configs/metrics.yaml", tp.Field(i).Name) |
| 50 | + } |
| 51 | + if tag := tp.Field(i).Tag.Get("metric"); tag == "" { |
| 52 | + t.Errorf("field %s is missing its `metric` tag", tp.Field(i).Name) |
| 53 | + } |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +// TestMetricsYAMLInvariants checks the embedded single-source-of-truth file for the |
| 58 | +// invariants the loader and the Grafana renderer both rely on: 1:1 correspondence with |
| 59 | +// the struct, unique prometheus names, the common prefix, and key/name being distinct |
| 60 | +// enough that the stable-key indirection holds (key never carries the prefix). |
| 61 | +func TestMetricsYAMLInvariants(t *testing.T) { |
| 62 | + var cfg metricsConfig |
| 63 | + if err := yaml.Unmarshal(configs.MetricsYAML, &cfg); err != nil { |
| 64 | + t.Fatalf("parsing embedded metrics.yaml: %v", err) |
| 65 | + } |
| 66 | + if cfg.Prefix == "" { |
| 67 | + t.Fatal("metrics.yaml: prefix must be set") |
| 68 | + } |
| 69 | + |
| 70 | + numFields := reflect.TypeOf(Metrics{}).NumField() |
| 71 | + if len(cfg.Metrics) != numFields { |
| 72 | + t.Errorf("metrics.yaml has %d entries but Metrics struct has %d fields (must be 1:1)", len(cfg.Metrics), numFields) |
| 73 | + } |
| 74 | + |
| 75 | + names := make(map[string]string, len(cfg.Metrics)) |
| 76 | + for key, def := range cfg.Metrics { |
| 77 | + if !strings.HasPrefix(def.Name, cfg.Prefix) { |
| 78 | + t.Errorf("metric %q: name %q does not start with prefix %q", key, def.Name, cfg.Prefix) |
| 79 | + } |
| 80 | + if strings.HasPrefix(key, cfg.Prefix) { |
| 81 | + t.Errorf("metric %q: stable key must not carry the %q prefix", key, cfg.Prefix) |
| 82 | + } |
| 83 | + if prev, dup := names[def.Name]; dup { |
| 84 | + t.Errorf("duplicate prometheus name %q (keys %q and %q)", def.Name, prev, key) |
| 85 | + } |
| 86 | + names[def.Name] = key |
| 87 | + } |
| 88 | +} |
0 commit comments