nflog: add initial tests
This commit is contained in:
parent
36b2e89f27
commit
48b6c8ff70
|
@ -142,11 +142,15 @@ func WithMaintenance(sf string, d time.Duration, stopc chan struct{}, done func(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func utcNow() time.Time {
|
||||||
|
return time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a new notification log based on the provided options.
|
// New creates a new notification log based on the provided options.
|
||||||
// The snapshot is loaded into the Log if it is set.
|
// The snapshot is loaded into the Log if it is set.
|
||||||
func New(opts ...Option) (Log, error) {
|
func New(opts ...Option) (Log, error) {
|
||||||
l := &nlog{
|
l := &nlog{
|
||||||
now: time.Now,
|
now: utcNow,
|
||||||
st: map[string]*pb.MeshEntry{},
|
st: map[string]*pb.MeshEntry{},
|
||||||
}
|
}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
|
@ -223,7 +227,7 @@ func (l *nlog) LogResolved(r *pb.Receiver, key, hash []byte) error {
|
||||||
return l.log(r, key, hash, true)
|
return l.log(r, key, hash, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stateKey returns a string key for a log entry consisting of entrie's group key
|
// stateKey returns a string key for a log entry consisting of the group key
|
||||||
// and receiver.
|
// and receiver.
|
||||||
func stateKey(k []byte, r *pb.Receiver) string {
|
func stateKey(k []byte, r *pb.Receiver) string {
|
||||||
return fmt.Sprintf("%s:%s", k, r)
|
return fmt.Sprintf("%s:%s", k, r)
|
||||||
|
@ -282,7 +286,7 @@ func (l *nlog) GC() (int, error) {
|
||||||
for k, le := range l.st {
|
for k, le := range l.st {
|
||||||
if ets, err := ptypes.Timestamp(le.ExpiresAt); err != nil {
|
if ets, err := ptypes.Timestamp(le.ExpiresAt); err != nil {
|
||||||
return n, err
|
return n, err
|
||||||
} else if ets.Before(now) {
|
} else if !ets.After(now) {
|
||||||
delete(l.st, k)
|
delete(l.st, k)
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
@ -448,6 +452,14 @@ func (gd gossipData) Encode() [][]byte {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gd gossipData) clone() gossipData {
|
||||||
|
res := make(gossipData, len(gd))
|
||||||
|
for k, e := range gd {
|
||||||
|
res[k] = e
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// Merge the notification set with gossip data and return a new notification
|
// Merge the notification set with gossip data and return a new notification
|
||||||
// state.
|
// state.
|
||||||
// TODO(fabxc): can we just return the receiver. Does it have to remain
|
// TODO(fabxc): can we just return the receiver. Does it have to remain
|
||||||
|
|
|
@ -1 +1,267 @@
|
||||||
package nflog
|
package nflog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/ptypes"
|
||||||
|
"github.com/golang/protobuf/ptypes/timestamp"
|
||||||
|
pb "github.com/prometheus/alertmanager/nflog/nflogpb"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNlogGC(t *testing.T) {
|
||||||
|
now := utcNow()
|
||||||
|
// We only care about key names and expiration timestamps.
|
||||||
|
newEntry := func(ts time.Time) *pb.MeshEntry {
|
||||||
|
return &pb.MeshEntry{
|
||||||
|
ExpiresAt: mustTimestampProto(ts),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l := &nlog{
|
||||||
|
st: gossipData{
|
||||||
|
"a1": newEntry(now),
|
||||||
|
"a2": newEntry(now.Add(time.Second)),
|
||||||
|
"a3": newEntry(now.Add(-time.Second)),
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
}
|
||||||
|
n, err := l.GC()
|
||||||
|
require.NoError(t, err, "unexpected error in garbage collection")
|
||||||
|
require.Equal(t, 2, n, "unexpected number of removed entries")
|
||||||
|
|
||||||
|
expected := gossipData{
|
||||||
|
"a2": newEntry(now.Add(time.Second)),
|
||||||
|
}
|
||||||
|
require.Equal(t, l.st, expected, "unepexcted state after garbage collection")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNlogSnapshot(t *testing.T) {
|
||||||
|
// Check whether storing and loading the snapshot is symmetric.
|
||||||
|
now := utcNow()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
entries []*pb.MeshEntry
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
entries: []*pb.MeshEntry{
|
||||||
|
{
|
||||||
|
Entry: &pb.Entry{
|
||||||
|
GroupKey: []byte("d8e8fca2dc0f896fd7cb4cb0031ba249"),
|
||||||
|
Receiver: &pb.Receiver{GroupName: "abc", Integration: "test1", Idx: 1},
|
||||||
|
GroupHash: []byte("126a8a51b9d1bbd07fddc65819a542c3"),
|
||||||
|
Resolved: false,
|
||||||
|
Timestamp: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
ExpiresAt: mustTimestampProto(now),
|
||||||
|
}, {
|
||||||
|
Entry: &pb.Entry{
|
||||||
|
GroupKey: []byte("d8e8fca2dc0f8abce7cb4cb0031ba249"),
|
||||||
|
Receiver: &pb.Receiver{GroupName: "def", Integration: "test2", Idx: 29},
|
||||||
|
GroupHash: []byte("122c2331b9d1bbd07fddc65819a542c3"),
|
||||||
|
Resolved: true,
|
||||||
|
Timestamp: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
ExpiresAt: mustTimestampProto(now),
|
||||||
|
}, {
|
||||||
|
Entry: &pb.Entry{
|
||||||
|
GroupKey: []byte("aaaaaca2dc0f896fd7cb4cb0031ba249"),
|
||||||
|
Receiver: &pb.Receiver{GroupName: "ghi", Integration: "test3", Idx: 0},
|
||||||
|
GroupHash: []byte("126a8a51b9d1bbd07fddc6e3e3e542c3"),
|
||||||
|
Resolved: false,
|
||||||
|
Timestamp: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
ExpiresAt: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
f, err := ioutil.TempFile("", "snapshot")
|
||||||
|
require.NoError(t, err, "creating temp file failed")
|
||||||
|
|
||||||
|
l1 := &nlog{st: gossipData{}}
|
||||||
|
// Setup internal state manually.
|
||||||
|
for _, e := range c.entries {
|
||||||
|
l1.st[stateKey(e.Entry.GroupKey, e.Entry.Receiver)] = e
|
||||||
|
}
|
||||||
|
_, err = l1.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.
|
||||||
|
l2 := &nlog{}
|
||||||
|
err = l2.loadSnapshot(f)
|
||||||
|
require.NoError(t, err, "error loading snapshot")
|
||||||
|
require.Equal(t, l1.st, l2.st, "state after loading snapshot did not match snapshotted state")
|
||||||
|
|
||||||
|
require.NoError(t, f.Close(), "closing snapshot file failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReplaceFile(t *testing.T) {
|
||||||
|
dir, err := ioutil.TempDir("", "replace_file")
|
||||||
|
require.NoError(t, err, "creating temp dir failed")
|
||||||
|
|
||||||
|
origFilename := filepath.Join(dir, "testfile")
|
||||||
|
|
||||||
|
of, err := os.Create(origFilename)
|
||||||
|
require.NoError(t, err, "creating file failed")
|
||||||
|
|
||||||
|
nf, err := openReplace(origFilename)
|
||||||
|
require.NoError(t, err, "opening replacement file failed")
|
||||||
|
|
||||||
|
_, err = nf.Write([]byte("test"))
|
||||||
|
require.NoError(t, err, "writing replace file failed")
|
||||||
|
|
||||||
|
require.NotEqual(t, nf.Name(), of.Name(), "replacement file must have different name while editing")
|
||||||
|
require.NoError(t, nf.Close(), "closing replacement file failed")
|
||||||
|
require.NoError(t, of.Close(), "closing original file failed")
|
||||||
|
|
||||||
|
ofr, err := os.Open(origFilename)
|
||||||
|
require.NoError(t, err, "opening original file failed")
|
||||||
|
defer ofr.Close()
|
||||||
|
|
||||||
|
res, err := ioutil.ReadAll(ofr)
|
||||||
|
require.NoError(t, err, "reading original file failed")
|
||||||
|
require.Equal(t, "test", string(res), "unexpected file contents")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGossipDataMerge(t *testing.T) {
|
||||||
|
now := utcNow()
|
||||||
|
|
||||||
|
// We only care about key names and timestamps for the
|
||||||
|
// merging logic.
|
||||||
|
newEntry := func(ts time.Time) *pb.MeshEntry {
|
||||||
|
return &pb.MeshEntry{
|
||||||
|
Entry: &pb.Entry{Timestamp: mustTimestampProto(ts)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
a, b gossipData
|
||||||
|
final, delta gossipData
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
a: gossipData{
|
||||||
|
"a1": newEntry(now),
|
||||||
|
"a2": newEntry(now),
|
||||||
|
"a3": newEntry(now),
|
||||||
|
},
|
||||||
|
b: gossipData{
|
||||||
|
"b1": newEntry(now), // new key, should be added
|
||||||
|
"a2": newEntry(now.Add(-time.Minute)), // older timestamp, should be dropped
|
||||||
|
"a3": newEntry(now.Add(time.Minute)), // newer timestamp, should overwrite
|
||||||
|
},
|
||||||
|
final: gossipData{
|
||||||
|
"a1": newEntry(now),
|
||||||
|
"a2": newEntry(now),
|
||||||
|
"a3": newEntry(now.Add(time.Minute)),
|
||||||
|
"b1": newEntry(now),
|
||||||
|
},
|
||||||
|
delta: gossipData{
|
||||||
|
"b1": newEntry(now),
|
||||||
|
"a3": newEntry(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")
|
||||||
|
|
||||||
|
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.MeshEntry
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
entries: []*pb.MeshEntry{
|
||||||
|
{
|
||||||
|
Entry: &pb.Entry{
|
||||||
|
GroupKey: []byte("d8e8fca2dc0f896fd7cb4cb0031ba249"),
|
||||||
|
Receiver: &pb.Receiver{GroupName: "abc", Integration: "test1", Idx: 1},
|
||||||
|
GroupHash: []byte("126a8a51b9d1bbd07fddc65819a542c3"),
|
||||||
|
Resolved: false,
|
||||||
|
Timestamp: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
ExpiresAt: mustTimestampProto(now),
|
||||||
|
}, {
|
||||||
|
Entry: &pb.Entry{
|
||||||
|
GroupKey: []byte("d8e8fca2dc0f8abce7cb4cb0031ba249"),
|
||||||
|
Receiver: &pb.Receiver{GroupName: "def", Integration: "test2", Idx: 29},
|
||||||
|
GroupHash: []byte("122c2331b9d1bbd07fddc65819a542c3"),
|
||||||
|
Resolved: true,
|
||||||
|
Timestamp: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
ExpiresAt: mustTimestampProto(now),
|
||||||
|
}, {
|
||||||
|
Entry: &pb.Entry{
|
||||||
|
GroupKey: []byte("aaaaaca2dc0f896fd7cb4cb0031ba249"),
|
||||||
|
Receiver: &pb.Receiver{GroupName: "ghi", Integration: "test3", Idx: 0},
|
||||||
|
GroupHash: []byte("126a8a51b9d1bbd07fddc6e3e3e542c3"),
|
||||||
|
Resolved: false,
|
||||||
|
Timestamp: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
ExpiresAt: mustTimestampProto(now),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
// Create gossip data from input.
|
||||||
|
in := gossipData{}
|
||||||
|
for _, e := range c.entries {
|
||||||
|
in[stateKey(e.Entry.GroupKey, e.Entry.Receiver)] = 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustTimestamp(ts *timestamp.Timestamp) time.Time {
|
||||||
|
res, err := ptypes.Timestamp(ts)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustTimestampProto(ts time.Time) *timestamp.Timestamp {
|
||||||
|
res, err := ptypes.TimestampProto(ts)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue