github.com/m-lab/locate@v0.17.6/heartbeat/heartbeat_test.go (about) 1 package heartbeat 2 3 import ( 4 "errors" 5 "reflect" 6 "testing" 7 "time" 8 9 "github.com/m-lab/locate/static" 10 11 "github.com/go-test/deep" 12 "github.com/m-lab/go/testingx" 13 v2 "github.com/m-lab/locate/api/v2" 14 "github.com/m-lab/locate/connection/testdata" 15 "github.com/m-lab/locate/heartbeat/heartbeattest" 16 "github.com/m-lab/locate/metrics" 17 prometheus "github.com/prometheus/client_model/go" 18 ) 19 20 var ( 21 fakeDC = &heartbeattest.FakeMemorystoreClient 22 fakeErrDC = &heartbeattest.FakeErrorMemorystoreClient 23 testMachine = "mlab1-lga00.mlab-sandbox.measurement-lab.org" 24 testHostname = "ndt-" + testMachine 25 ) 26 27 func TestRegisterInstance_PutError(t *testing.T) { 28 h := NewHeartbeatStatusTracker(fakeErrDC) 29 defer h.StopImport() 30 31 err := h.RegisterInstance(*testdata.FakeRegistration.Registration) 32 33 if !errors.Is(err, heartbeattest.FakeError) { 34 t.Errorf("RegisterInstance() error: %+v, want: %+v", err, heartbeattest.FakeError) 35 } 36 } 37 38 func TestRegisterInstance_Success(t *testing.T) { 39 h := NewHeartbeatStatusTracker(fakeDC) 40 defer h.StopImport() 41 42 hbm := testdata.FakeRegistration 43 err := h.RegisterInstance(*hbm.Registration) 44 45 if err != nil { 46 t.Errorf("RegisterInstance() error: %+v, want: nil", err) 47 } 48 49 if diff := deep.Equal(h.instances[hbm.Registration.Hostname], hbm); diff != nil { 50 t.Errorf("RegisterInstance() failed to register; got: %+v. want: %+v", 51 h.instances[hbm.Registration.Hostname], &hbm) 52 } 53 } 54 55 func TestRegisterInstanceTwice(t *testing.T) { 56 h := NewHeartbeatStatusTracker(fakeDC) 57 defer h.StopImport() 58 59 // Register once. 60 reg := testdata.FakeRegistration.Registration 61 err := h.RegisterInstance(*reg) 62 testingx.Must(t, err, "failed to register instance") 63 64 // Set health. 65 err = h.UpdateHealth(testdata.FakeHostname, v2.Health{Score: 1.0}) 66 testingx.Must(t, err, "failed to update health") 67 68 // Re-register. 69 newReg := v2.Registration(*testdata.FakeRegistration.Registration) 70 newReg.Site = "foo" 71 err = h.RegisterInstance(newReg) 72 73 got := h.instances[reg.Hostname] 74 if got.Registration.Site != "foo" { 75 t.Errorf("RegisterInstance() failed to re-register; got: %+v, want: %+v", got.Registration.Site, "foo") 76 } 77 78 if got.Health.Score != 1.0 { 79 t.Errorf("RegisterInstance() changed health; got: %+v, want: %+v", got.Health.Score, "foo") 80 } 81 } 82 83 func TestUpdateHealth_UpdateError(t *testing.T) { 84 h := NewHeartbeatStatusTracker(fakeErrDC) 85 defer h.StopImport() 86 87 hm := testdata.FakeHealth.Health 88 err := h.UpdateHealth(testdata.FakeHostname, *hm) 89 90 if !errors.Is(err, heartbeattest.FakeError) { 91 t.Errorf("UpdateHealth() error: %+v, want: %+v", err, heartbeattest.FakeError) 92 } 93 } 94 95 func TestUpdateHealth_LocalError(t *testing.T) { 96 h := NewHeartbeatStatusTracker(fakeDC) 97 defer h.StopImport() 98 99 hm := testdata.FakeHealth.Health 100 err := h.UpdateHealth(testdata.FakeHostname, *hm) 101 102 if err == nil { 103 t.Error("UpdateHealth() error: nil, want: !nil") 104 } 105 } 106 107 func TestUpdateHealth_Success(t *testing.T) { 108 h := NewHeartbeatStatusTracker(fakeDC) 109 defer h.StopImport() 110 111 err := h.RegisterInstance(*testdata.FakeRegistration.Registration) 112 testingx.Must(t, err, "failed to register instance") 113 114 hm := testdata.FakeHealth.Health 115 err = h.UpdateHealth(testdata.FakeHostname, *hm) 116 117 if err != nil { 118 t.Errorf("UpdateHealth() error: %+v, want: !nil", err) 119 } 120 121 if diff := deep.Equal(h.instances[testdata.FakeHostname].Health, hm); diff != nil { 122 t.Errorf("UpdateHealth() failed to update health; got: %+v, want: %+v", 123 h.instances[testdata.FakeHostname].Health, hm) 124 } 125 } 126 127 func TestUpdatePrometheus_PutError(t *testing.T) { 128 h := heartbeatStatusTracker{ 129 MemorystoreClient: fakeErrDC, 130 instances: map[string]v2.HeartbeatMessage{ 131 testHostname: { 132 Registration: &v2.Registration{ 133 Hostname: testHostname, 134 }, 135 }, 136 }, 137 } 138 hostnames := map[string]bool{testHostname: true} 139 machines := map[string]bool{testMachine: true} 140 141 err := h.UpdatePrometheus(hostnames, machines) 142 143 if !errors.Is(err, errPrometheus) { 144 t.Errorf("UpdatePrometheus() err: %v, want: %v", err, errPrometheus) 145 } 146 } 147 148 func TestUpdatePrometheus_Success(t *testing.T) { 149 h := heartbeatStatusTracker{ 150 MemorystoreClient: fakeDC, 151 instances: map[string]v2.HeartbeatMessage{ 152 testHostname: { 153 Registration: &v2.Registration{ 154 Hostname: testHostname, 155 }, 156 }, 157 }, 158 } 159 hostnames := map[string]bool{testHostname: true} 160 machines := map[string]bool{testMachine: true} 161 162 err := h.UpdatePrometheus(hostnames, machines) 163 164 if err != nil { 165 t.Errorf("UpdatePrometheus() err: %v, want: nil", err) 166 } 167 } 168 169 func TestInstances(t *testing.T) { 170 h := NewHeartbeatStatusTracker(fakeDC) 171 h.StopImport() 172 173 hbm := testdata.FakeRegistration 174 h.RegisterInstance(*hbm.Registration) 175 176 instances := h.Instances() 177 expected := map[string]v2.HeartbeatMessage{testdata.FakeHostname: testdata.FakeRegistration} 178 if diff := deep.Equal(instances, expected); diff != nil { 179 t.Errorf("Instances() got: %+v, want: %+v", instances, expected) 180 } 181 182 } 183 184 func TestInstancesCopy(t *testing.T) { 185 h := NewHeartbeatStatusTracker(fakeDC) 186 h.StopImport() 187 188 // Add a new instance with nil v2.Health. 189 hbm := testdata.FakeRegistration 190 h.RegisterInstance(*hbm.Registration) 191 192 // Get copy of instances and verify that v2.Health field is nil. 193 instances := h.Instances() 194 if instances[testdata.FakeHostname].Health != nil { 195 t.Errorf("Instances() got: %+v, want: nil", instances[testdata.FakeHostname].Health) 196 } 197 198 // Update v2.Health for the instance in the tracker. 199 h.UpdateHealth(testdata.FakeHostname, *testdata.FakeHealth.Health) 200 instancesWithUpdate := h.Instances() 201 if instancesWithUpdate[testdata.FakeHostname].Health == nil { 202 t.Errorf("Instances() got: nil, want: %+v", instancesWithUpdate[testdata.FakeHostname].Health) 203 } 204 205 // Verify original copy of instances did not get updated. 206 if instances[testdata.FakeHostname].Health != nil { 207 t.Errorf("Instances() got: %+v, want: nil", instances[testdata.FakeHostname].Health) 208 } 209 } 210 211 func TestImportMemorystore(t *testing.T) { 212 fdc := &heartbeattest.FakeMemorystoreClient 213 h := NewHeartbeatStatusTracker(fdc) 214 if h.Ready() { 215 t.Errorf("importMemorystore() Ready too soon; got %s, want over: %s", time.Since(h.lastUpdate), 2*static.MemorystoreExportPeriod) 216 } 217 defer h.StopImport() 218 219 fdc.FakeAdd(testdata.FakeHostname, testdata.FakeRegistration) 220 h.importMemorystore() 221 222 expected := map[string]v2.HeartbeatMessage{testdata.FakeHostname: testdata.FakeRegistration} 223 if diff := deep.Equal(h.instances, expected); diff != nil { 224 t.Errorf("importMemorystore() failed to import; got: %+v, want: %+v", h.instances, 225 expected) 226 } 227 228 if !h.Ready() { 229 t.Errorf("importMemorystore() not Ready; got %s, want under: %s", time.Since(h.lastUpdate), 2*static.MemorystoreExportPeriod) 230 } 231 } 232 233 func TestUpdateMetrics(t *testing.T) { 234 tests := []struct { 235 name string 236 instances map[string]v2.HeartbeatMessage 237 experiment string 238 want float64 239 }{ 240 { 241 name: "success", 242 instances: map[string]v2.HeartbeatMessage{ 243 testdata.FakeHostname: { 244 Registration: testdata.FakeRegistration.Registration, 245 Health: testdata.FakeHealth.Health, 246 }, 247 }, 248 experiment: testdata.FakeRegistration.Registration.Experiment, 249 want: 1, 250 }, 251 { 252 name: "no-metrics", 253 instances: map[string]v2.HeartbeatMessage{}, 254 experiment: "", 255 want: 0, 256 }, 257 } 258 259 for _, tt := range tests { 260 t.Run(tt.name, func(t *testing.T) { 261 h := heartbeatStatusTracker{ 262 instances: tt.instances, 263 } 264 265 metrics.LocateHealthStatus.Reset() 266 h.updateMetrics() 267 268 metric := &prometheus.Metric{} 269 gauge := metrics.LocateHealthStatus.With(map[string]string{"experiment": tt.experiment}) 270 gauge.Write(metric) 271 got := metric.GetGauge().GetValue() 272 273 if got != tt.want { 274 t.Errorf("updateMetrics() failed; got: %f want %f", got, tt.want) 275 } 276 }) 277 } 278 } 279 280 func TestGetPrometheusMessage(t *testing.T) { 281 tests := []struct { 282 name string 283 hostnames map[string]bool 284 machines map[string]bool 285 reg *v2.Registration 286 want *v2.Prometheus 287 }{ 288 { 289 name: "nil-registration", 290 hostnames: map[string]bool{testHostname: true}, 291 machines: map[string]bool{testMachine: true}, 292 reg: nil, 293 want: nil, 294 }, 295 { 296 name: "both-empty", 297 hostnames: map[string]bool{}, 298 machines: map[string]bool{}, 299 reg: &v2.Registration{ 300 Hostname: testHostname, 301 }, 302 want: nil, 303 }, 304 { 305 name: "only-hostnames", 306 hostnames: map[string]bool{testHostname: true}, 307 machines: map[string]bool{}, 308 reg: &v2.Registration{ 309 Hostname: testHostname, 310 }, 311 want: &v2.Prometheus{Health: true}, 312 }, 313 { 314 name: "only-machines", 315 hostnames: map[string]bool{}, 316 machines: map[string]bool{testMachine: true}, 317 reg: &v2.Registration{ 318 Hostname: testHostname, 319 }, 320 want: &v2.Prometheus{Health: true}, 321 }, 322 { 323 name: "both-unhealthy", 324 hostnames: map[string]bool{testHostname: false}, 325 machines: map[string]bool{testMachine: false}, 326 reg: &v2.Registration{ 327 Hostname: testHostname, 328 }, 329 want: &v2.Prometheus{Health: false}, 330 }, 331 { 332 name: "only-hostname-unhealthy", 333 hostnames: map[string]bool{testHostname: false}, 334 machines: map[string]bool{testMachine: true}, 335 reg: &v2.Registration{ 336 Hostname: testHostname, 337 }, 338 want: &v2.Prometheus{Health: false}, 339 }, 340 { 341 name: "only-machine-unhealthy", 342 hostnames: map[string]bool{testHostname: true}, 343 machines: map[string]bool{testMachine: false}, 344 reg: &v2.Registration{ 345 Hostname: testHostname, 346 }, 347 want: &v2.Prometheus{Health: false}, 348 }, 349 { 350 name: "both-healthy", 351 hostnames: map[string]bool{testHostname: true}, 352 machines: map[string]bool{testMachine: true}, 353 reg: &v2.Registration{ 354 Hostname: testHostname, 355 }, 356 want: &v2.Prometheus{Health: true}, 357 }, 358 } 359 360 for _, tt := range tests { 361 t.Run(tt.name, func(t *testing.T) { 362 i := v2.HeartbeatMessage{Registration: tt.reg} 363 pm := constructPrometheusMessage(i, tt.hostnames, tt.machines) 364 365 if !reflect.DeepEqual(pm, tt.want) { 366 t.Errorf("getPrometheusMessage() got: %v, want: %v", pm, tt.want) 367 } 368 }) 369 } 370 }