// Copyright 2016 Prometheus Team // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package silence import ( "bytes" "io/ioutil" "os" "sort" "strings" "testing" "time" pb "github.com/prometheus/alertmanager/silence/silencepb" "github.com/prometheus/alertmanager/types" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "github.com/weaveworks/mesh" ) func TestOptionsValidate(t *testing.T) { cases := []struct { options *Options err string }{ { options: &Options{ SnapshotReader: &bytes.Buffer{}, }, }, { options: &Options{ SnapshotFile: "test.bkp", }, }, { options: &Options{ SnapshotFile: "test bkp", SnapshotReader: &bytes.Buffer{}, }, err: "only one of SnapshotFile and SnapshotReader must be set", }, } for _, c := range cases { err := c.options.validate() if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestSilencesGC(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := utcNow() s.now = func() time.Time { return now } newSilence := func(exp time.Time) *pb.MeshSilence { return &pb.MeshSilence{ExpiresAt: exp} } s.st = &gossipData{ data: silenceMap{ "1": newSilence(now), "2": newSilence(now.Add(-time.Second)), "3": newSilence(now.Add(time.Second)), }, } want := &gossipData{ data: silenceMap{ "3": newSilence(now.Add(time.Second)), }, } n, err := s.GC() require.NoError(t, err) require.Equal(t, 2, n) require.Equal(t, want, s.st) } func TestSilencesSnapshot(t *testing.T) { // Check whether storing and loading the snapshot is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshSilence }{ { entries: []*pb.MeshSilence{ { Silence: &pb.Silence{ Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, }, StartsAt: now, EndsAt: now, UpdatedAt: now, }, ExpiresAt: now, }, { Silence: &pb.Silence{ Id: "4b1e760d-182c-4980-b873-c1a6827c9817", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, }, StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now, }, ExpiresAt: now.Add(24 * time.Hour), }, }, }, } for _, c := range cases { f, err := ioutil.TempFile("", "snapshot") require.NoError(t, err, "creating temp file failed") s1 := &Silences{st: newGossipData(), metrics: newMetrics(nil, nil)} // Setup internal state manually. for _, e := range c.entries { s1.st.data[e.Silence.Id] = e } _, err = s1.Snapshot(f) require.NoError(t, err, "creating snapshot failed") require.NoError(t, f.Close(), "closing snapshot file failed") f, err = os.Open(f.Name()) require.NoError(t, err, "opening snapshot file failed") // Check again against new nlog instance. s2 := &Silences{mc: matcherCache{}, st: newGossipData()} err = s2.loadSnapshot(f) require.NoError(t, err, "error loading snapshot") require.Equal(t, s1.st.data, s2.st.data, "state after loading snapshot did not match snapshotted state") require.NoError(t, f.Close(), "closing snapshot file failed") } } type mockGossip struct { broadcast func(mesh.GossipData) } func (g *mockGossip) GossipBroadcast(d mesh.GossipData) { g.broadcast(d) } func (g *mockGossip) GossipUnicast(mesh.PeerName, []byte) error { panic("not implemented") } func TestSilencesSetSilence(t *testing.T) { s, err := New(Options{ Retention: time.Minute, }) require.NoError(t, err) now := utcNow() nowpb := now sil := &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{{Name: "abc", Pattern: "def"}}, StartsAt: nowpb, EndsAt: nowpb, } want := &gossipData{ data: silenceMap{ "some_id": &pb.MeshSilence{ Silence: sil, ExpiresAt: now.Add(time.Minute), }, }, } done := make(chan bool) s.gossip = &mockGossip{ broadcast: func(d mesh.GossipData) { data, ok := d.(*gossipData) // Double check that we can take a lock on s.mtx here. s.mtx.Lock() defer s.mtx.Unlock() require.True(t, ok, "gossip data of unknown type") require.Equal(t, want.data, data.data, "unexpected gossip broadcast data") close(done) }, } // setSilence() is always called with s.mtx locked() go func() { s.mtx.Lock() require.NoError(t, s.setSilence(sil)) s.mtx.Unlock() }() // GossipBroadcast is called in a goroutine. select { case <-done: case <-time.After(1 * time.Second): t.Fatal("GossipBroadcast was not called") } require.Equal(t, want.data, s.st.data, "Unexpected silence state") } func TestSilenceSet(t *testing.T) { s, err := New(Options{ Retention: time.Hour, }) require.NoError(t, err) now := utcNow() now1 := now s.now = func() time.Time { return now } // Insert silence with fixed start time. sil1 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now.Add(2 * time.Minute), EndsAt: now.Add(5 * time.Minute), } id1, err := s.Set(sil1) require.NoError(t, err) require.NotEqual(t, id1, "") want := &gossipData{ data: silenceMap{ id1: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id1, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now1.Add(2 * time.Minute), EndsAt: now1.Add(5 * time.Minute), UpdatedAt: now1, }, ExpiresAt: now1.Add(5*time.Minute + s.retention), }, }, } require.Equal(t, want.data, s.st.data, "unexpected state after silence creation") // Insert silence with unset start time. Must be set to now. now = now.Add(time.Minute) now2 := now sil2 := &pb.Silence{ Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, EndsAt: now.Add(1 * time.Minute), } id2, err := s.Set(sil2) require.NoError(t, err) require.NotEqual(t, id2, "") want = &gossipData{ data: silenceMap{ id1: want.data[id1], id2: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id2, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now2, EndsAt: now2.Add(1 * time.Minute), UpdatedAt: now2, }, ExpiresAt: now2.Add(1*time.Minute + s.retention), }, }, } require.Equal(t, want.data, s.st.data, "unexpected state after silence creation") // Overwrite silence 2 with new end time. now = now.Add(time.Minute) now3 := now sil3 := cloneSilence(sil2) sil3.EndsAt = now.Add(100 * time.Minute) id3, err := s.Set(sil3) require.NoError(t, err) require.Equal(t, id2, id3) want = &gossipData{ data: silenceMap{ id1: want.data[id1], id2: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id2, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now2, EndsAt: now3.Add(100 * time.Minute), UpdatedAt: now3, }, ExpiresAt: now3.Add(100*time.Minute + s.retention), }, }, } require.Equal(t, want.data, s.st.data, "unexpected state after silence creation") // Update silence 2 with new matcher expires it and creates a new one. now = now.Add(time.Minute) now4 := now sil4 := cloneSilence(sil3) sil4.Matchers = []*pb.Matcher{{Name: "a", Pattern: "c"}} id4, err := s.Set(sil4) require.NoError(t, err) require.NotEqual(t, id2, id4) want = &gossipData{ data: silenceMap{ id1: want.data[id1], id2: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id2, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now2, EndsAt: now4, UpdatedAt: now4, }, ExpiresAt: now4.Add(s.retention), }, id4: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id4, Matchers: []*pb.Matcher{{Name: "a", Pattern: "c"}}, StartsAt: now4, EndsAt: now3.Add(100 * time.Minute), UpdatedAt: now4, }, ExpiresAt: now3.Add(100*time.Minute + s.retention), }, }, } require.Equal(t, want.data, s.st.data, "unexpected state after silence creation") // Re-create expired silence. now = now.Add(time.Minute) now5 := now sil5 := cloneSilence(sil3) sil5.StartsAt = now sil5.EndsAt = now.Add(5 * time.Minute) id5, err := s.Set(sil5) require.NoError(t, err) require.NotEqual(t, id2, id4) want = &gossipData{ data: silenceMap{ id1: want.data[id1], id2: want.data[id2], id4: want.data[id4], id5: &pb.MeshSilence{ Silence: &pb.Silence{ Id: id5, Matchers: []*pb.Matcher{{Name: "a", Pattern: "b"}}, StartsAt: now5, EndsAt: now5.Add(5 * time.Minute), UpdatedAt: now5, }, ExpiresAt: now5.Add(5*time.Minute + s.retention), }, }, } require.Equal(t, want.data, s.st.data, "unexpected state after silence creation") } func TestSilencesSetFail(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := utcNow() s.now = func() time.Time { return now } cases := []struct { s *pb.Silence err string }{ { s: &pb.Silence{Id: "some_id"}, err: ErrNotFound.Error(), }, { s: &pb.Silence{}, // Silence without matcher. err: "silence invalid", }, } for _, c := range cases { _, err := s.Set(c.s) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestQState(t *testing.T) { now := utcNow() cases := []struct { sil *pb.Silence states []types.SilenceState keep bool }{ { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []types.SilenceState{types.SilenceStateActive, types.SilenceStateExpired}, keep: false, }, { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []types.SilenceState{types.SilenceStatePending}, keep: true, }, { sil: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), }, states: []types.SilenceState{types.SilenceStateExpired, types.SilenceStatePending}, keep: true, }, } for i, c := range cases { q := &query{} QState(c.states...)(q) f := q.filters[0] keep, err := f(c.sil, nil, now) require.NoError(t, err) require.Equal(t, c.keep, keep, "unexpected filter result for case %d", i) } } func TestQMatches(t *testing.T) { qp := QMatches(model.LabelSet{ "job": "test", "instance": "web-1", "path": "/user/profile", "method": "GET", }) q := &query{} qp(q) f := q.filters[0] cases := []struct { sil *pb.Silence drop bool }{ { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, }, }, drop: true, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "job", Pattern: "test", Type: pb.Matcher_EQUAL}, {Name: "method", Pattern: "POST", Type: pb.Matcher_EQUAL}, }, }, drop: false, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, }, }, drop: true, }, { sil: &pb.Silence{ Matchers: []*pb.Matcher{ {Name: "path", Pattern: "/user/.+", Type: pb.Matcher_REGEXP}, {Name: "path", Pattern: "/nothing/.+", Type: pb.Matcher_REGEXP}, }, }, drop: false, }, } for _, c := range cases { drop, err := f(c.sil, &Silences{mc: matcherCache{}, st: newGossipData()}, time.Time{}) require.NoError(t, err) require.Equal(t, c.drop, drop, "unexpected filter result") } } func TestSilencesQuery(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) s.st = &gossipData{ data: silenceMap{ "1": &pb.MeshSilence{Silence: &pb.Silence{Id: "1"}}, "2": &pb.MeshSilence{Silence: &pb.Silence{Id: "2"}}, "3": &pb.MeshSilence{Silence: &pb.Silence{Id: "3"}}, "4": &pb.MeshSilence{Silence: &pb.Silence{Id: "4"}}, "5": &pb.MeshSilence{Silence: &pb.Silence{Id: "5"}}, }, } cases := []struct { q *query exp []*pb.Silence }{ { // Default query of retrieving all silences. q: &query{}, exp: []*pb.Silence{ {Id: "1"}, {Id: "2"}, {Id: "3"}, {Id: "4"}, {Id: "5"}, }, }, { // Retrieve by IDs. q: &query{ ids: []string{"2", "5"}, }, exp: []*pb.Silence{ {Id: "2"}, {Id: "5"}, }, }, { // Retrieve all and filter q: &query{ filters: []silenceFilter{ func(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) { return sil.Id == "1" || sil.Id == "2", nil }, }, }, exp: []*pb.Silence{ {Id: "1"}, {Id: "2"}, }, }, { // Retrieve by IDs and filter q: &query{ ids: []string{"2", "5"}, filters: []silenceFilter{ func(sil *pb.Silence, _ *Silences, _ time.Time) (bool, error) { return sil.Id == "1" || sil.Id == "2", nil }, }, }, exp: []*pb.Silence{ {Id: "2"}, }, }, } for _, c := range cases { // Run default query of retrieving all silences. res, err := s.query(c.q, time.Time{}) require.NoError(t, err, "unexpected error on querying") // Currently there are no sorting guarantees in the querying API. sort.Sort(silencesByID(c.exp)) sort.Sort(silencesByID(res)) require.Equal(t, c.exp, res, "unexpected silences in result") } } type silencesByID []*pb.Silence func (s silencesByID) Len() int { return len(s) } func (s silencesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s silencesByID) Less(i, j int) bool { return s[i].Id < s[j].Id } func TestSilenceCanUpdate(t *testing.T) { now := utcNow() cases := []struct { a, b *pb.Silence ok bool }{ // Bad arguments. { a: &pb.Silence{}, b: &pb.Silence{ StartsAt: now, EndsAt: now.Add(-time.Minute), }, ok: false, }, // Expired silence. { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Second), }, b: &pb.Silence{ StartsAt: now, EndsAt: now, }, ok: false, }, // Pending silences. { a: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour), }, ok: false, }, { a: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Minute), }, ok: true, }, { a: &pb.Silence{ StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now, // set to exactly start now. EndsAt: now.Add(2 * time.Hour), }, ok: true, }, // Active silences. { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Minute), EndsAt: now.Add(2 * time.Hour), }, ok: false, }, { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Second), }, ok: false, }, { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now, }, ok: true, }, { a: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now.Add(-time.Hour), }, b: &pb.Silence{ StartsAt: now.Add(-time.Hour), EndsAt: now.Add(3 * time.Hour), }, ok: true, }, } for _, c := range cases { ok := canUpdate(c.a, c.b, now) if ok && !c.ok { t.Errorf("expected not-updateable but was: %v, %v", c.a, c.b) } if ok && !c.ok { t.Errorf("expected updateable but was not: %v, %v", c.a, c.b) } } } func TestSilenceExpire(t *testing.T) { s, err := New(Options{}) require.NoError(t, err) now := time.Now() s.now = func() time.Time { return now } m := &pb.Matcher{Type: pb.Matcher_EQUAL, Name: "a", Pattern: "b"} s.st = &gossipData{ data: silenceMap{ "pending": &pb.MeshSilence{Silence: &pb.Silence{ Id: "pending", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(time.Minute), EndsAt: now.Add(time.Hour), UpdatedAt: now.Add(-time.Hour), }}, "active": &pb.MeshSilence{Silence: &pb.Silence{ Id: "active", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Minute), EndsAt: now.Add(time.Hour), UpdatedAt: now.Add(-time.Hour), }}, "expired": &pb.MeshSilence{Silence: &pb.Silence{ Id: "expired", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Minute), UpdatedAt: now.Add(-time.Hour), }}, }, } count, err := s.CountState(types.SilenceStatePending) require.NoError(t, err) require.Equal(t, 1, count) require.NoError(t, s.expire("pending")) require.NoError(t, s.expire("active")) err = s.expire("expired") require.Error(t, err) require.Contains(t, err.Error(), "already expired") sil, err := s.QueryOne(QIDs("pending")) require.NoError(t, err) require.Equal(t, &pb.Silence{ Id: "pending", Matchers: []*pb.Matcher{m}, StartsAt: now, EndsAt: now, UpdatedAt: now, }, sil) count, err = s.CountState(types.SilenceStatePending) require.NoError(t, err) require.Equal(t, 0, count) // Expiring a pending Silence should make the API return the // SilenceStateExpired Silence state. silenceState := types.CalcSilenceState(sil.StartsAt, sil.EndsAt) require.Equal(t, silenceState, types.SilenceStateExpired) sil, err = s.QueryOne(QIDs("active")) require.NoError(t, err) require.Equal(t, &pb.Silence{ Id: "active", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Minute), EndsAt: now, UpdatedAt: now, }, sil) sil, err = s.QueryOne(QIDs("expired")) require.NoError(t, err) require.Equal(t, &pb.Silence{ Id: "expired", Matchers: []*pb.Matcher{m}, StartsAt: now.Add(-time.Hour), EndsAt: now.Add(-time.Minute), UpdatedAt: now.Add(-time.Hour), }, sil) } func TestValidateMatcher(t *testing.T) { cases := []struct { m *pb.Matcher err string }{ { m: &pb.Matcher{ Name: "a", Pattern: "b", Type: pb.Matcher_EQUAL, }, err: "", }, { m: &pb.Matcher{ Name: "00", Pattern: "a", Type: pb.Matcher_EQUAL, }, err: "invalid label name", }, { m: &pb.Matcher{ Name: "a", Pattern: "((", Type: pb.Matcher_REGEXP, }, err: "invalid regular expression", }, { m: &pb.Matcher{ Name: "a", Pattern: "\xff", Type: pb.Matcher_EQUAL, }, err: "invalid label value", }, { m: &pb.Matcher{ Name: "a", Pattern: "b", Type: 333, }, err: "unknown matcher type", }, } for _, c := range cases { err := validateMatcher(c.m) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestValidateSilence(t *testing.T) { var ( now = utcNow() zeroTimestamp = time.Time{} validTimestamp = now ) cases := []struct { s *pb.Silence err string }{ { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "", }, { s: &pb.Silence{ Id: "", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "ID missing", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{}, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "at least one matcher required", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, &pb.Matcher{Name: "00", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "invalid label matcher", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: now, EndsAt: now.Add(-time.Second), UpdatedAt: validTimestamp, }, err: "end time must not be before start time", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: zeroTimestamp, EndsAt: validTimestamp, UpdatedAt: validTimestamp, }, err: "invalid zero start timestamp", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: zeroTimestamp, UpdatedAt: validTimestamp, }, err: "invalid zero end timestamp", }, { s: &pb.Silence{ Id: "some_id", Matchers: []*pb.Matcher{ &pb.Matcher{Name: "a", Pattern: "b"}, }, StartsAt: validTimestamp, EndsAt: validTimestamp, UpdatedAt: zeroTimestamp, }, err: "invalid zero update timestamp", }, } for _, c := range cases { err := validateSilence(c.s) if err == nil { if c.err != "" { t.Errorf("expected error containing %q but got none", c.err) } continue } if err != nil && c.err == "" { t.Errorf("unexpected error %q", err) continue } if !strings.Contains(err.Error(), c.err) { t.Errorf("expected error to contain %q but got %q", c.err, err) } } } func TestGossipDataMerge(t *testing.T) { now := utcNow() // We only care about key names and timestamps for the // merging logic. newSilence := func(ts time.Time) *pb.MeshSilence { return &pb.MeshSilence{ Silence: &pb.Silence{UpdatedAt: ts}, } } cases := []struct { a, b *gossipData final, delta *gossipData }{ { a: &gossipData{ data: silenceMap{ "a1": newSilence(now), "a2": newSilence(now), "a3": newSilence(now), }, }, b: &gossipData{ data: silenceMap{ "b1": newSilence(now), // new key, should be added "a2": newSilence(now.Add(-time.Minute)), // older timestamp, should be dropped "a3": newSilence(now.Add(time.Minute)), // newer timestamp, should overwrite }, }, final: &gossipData{ data: silenceMap{ "a1": newSilence(now), "a2": newSilence(now), "a3": newSilence(now.Add(time.Minute)), "b1": newSilence(now), }, }, delta: &gossipData{ data: silenceMap{ "b1": newSilence(now), "a3": newSilence(now.Add(time.Minute)), }, }, }, } for _, c := range cases { ca, cb := c.a.clone(), c.b.clone() res := ca.Merge(cb) require.Equal(t, c.final, res, "Merge result should match expectation") require.Equal(t, c.final, ca, "Merge should apply changes to original state") require.Equal(t, c.b, cb, "Merged state should remain unmodified") } } func TestGossipDataMergeDelta(t *testing.T) { now := utcNow() // We only care about key names and timestamps for the // merging logic. newSilence := func(ts time.Time) *pb.MeshSilence { return &pb.MeshSilence{ Silence: &pb.Silence{UpdatedAt: ts}, ExpiresAt: ts.Add(time.Hour), } } newExpiredSilence := func(ts time.Time) *pb.MeshSilence { return &pb.MeshSilence{ Silence: &pb.Silence{UpdatedAt: ts}, ExpiresAt: ts, } } cases := []struct { a, b *gossipData final, delta *gossipData }{ { a: &gossipData{ data: silenceMap{ "a1": newSilence(now), "a2": newSilence(now), "a3": newSilence(now), }, }, b: &gossipData{ data: silenceMap{ "b1": newSilence(now), // new key, should be added "a2": newSilence(now.Add(-time.Minute)), // older timestamp, should be dropped "a3": newSilence(now.Add(time.Minute)), // newer timestamp, should overwrite "a4": newExpiredSilence(now.Add(-time.Minute)), // expired, should be dropped "a5": newExpiredSilence(now.Add(-time.Hour)), // expired, should be dropped }, }, final: &gossipData{ data: silenceMap{ "a1": newSilence(now), "a2": newSilence(now), "a3": newSilence(now.Add(time.Minute)), "b1": newSilence(now), }, }, delta: &gossipData{ data: silenceMap{ "b1": newSilence(now), "a3": newSilence(now.Add(time.Minute)), }, }, }, } for _, c := range cases { ca, cb := c.a.clone(), c.b.clone() delta := ca.mergeDelta(cb) require.Equal(t, c.delta, delta, "Merge delta should match expectation") require.Equal(t, c.final, ca, "Merge should apply changes to original state") require.Equal(t, c.b, cb, "Merged state should remain unmodified") } } func TestGossipDataCoding(t *testing.T) { // Check whether encoding and decoding the data is symmetric. now := utcNow() cases := []struct { entries []*pb.MeshSilence }{ { entries: []*pb.MeshSilence{ { Silence: &pb.Silence{ Id: "3be80475-e219-4ee7-b6fc-4b65114e362f", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, {Name: "label2", Pattern: "val.+", Type: pb.Matcher_REGEXP}, }, StartsAt: now, EndsAt: now, UpdatedAt: now, }, ExpiresAt: now, }, { Silence: &pb.Silence{ Id: "4b1e760d-182c-4980-b873-c1a6827c9817", Matchers: []*pb.Matcher{ {Name: "label1", Pattern: "val1", Type: pb.Matcher_EQUAL}, }, StartsAt: now.Add(time.Hour), EndsAt: now.Add(2 * time.Hour), UpdatedAt: now, }, ExpiresAt: now.Add(24 * time.Hour), }, }, }, } for _, c := range cases { // Create gossip data from input. in := newGossipData() for _, e := range c.entries { in.data[e.Silence.Id] = e } msg := in.Encode() require.Equal(t, 1, len(msg), "expected single message for input") out, err := decodeGossipData(msg[0]) require.NoError(t, err, "decoding message failed") require.Equal(t, in, out, "decoded data doesn't match encoded data") } }