From 98e38be9fbbe4b8b77e21ec29d811f887785954d Mon Sep 17 00:00:00 2001 From: Andrew Cholakian Date: Thu, 31 Jan 2019 13:54:55 -0600 Subject: [PATCH] [Heartbeat] Fix id/summary with multi-url configs (#10408) [Heartbeat] Fix id/summary with multi-url configs When using configurations that specify multiple URLs in the config there were two issues: 1. They would all share the same ID, when IDs are supposed to be unique to an URL 2. The Job summary would summarize all the URLs, instead of all the DNS entries per URL. This patch fixes this by introducing a new WrapAllSeparately wrapper that wraps top level jobs with a new JobWrapperFactory, that lets us create JobWrapper instances with separate scopes. --- heartbeat/monitors/active/http/http_test.go | 13 +- heartbeat/monitors/active/tcp/tcp_test.go | 9 +- heartbeat/monitors/jobs/job.go | 22 +- heartbeat/monitors/jobs/job_test.go | 3 +- heartbeat/monitors/monitor_test.go | 5 +- heartbeat/monitors/wrappers/monitors.go | 61 ++-- heartbeat/monitors/wrappers/monitors_test.go | 293 +++++++++++------- heartbeat/monitors/wrappers/util_test.go | 3 +- libbeat/common/mapval/is_defs.go | 28 ++ libbeat/common/mapval/is_defs_test.go | 56 ++++ .../mapval/testing.go} | 9 +- libbeat/testing/mapvaltest/mapvaltest_test.go | 37 --- 12 files changed, 341 insertions(+), 198 deletions(-) rename libbeat/{testing/mapvaltest/mapvaltest.go => common/mapval/testing.go} (79%) delete mode 100644 libbeat/testing/mapvaltest/mapvaltest_test.go diff --git a/heartbeat/monitors/active/http/http_test.go b/heartbeat/monitors/active/http/http_test.go index 4822dc0cb68..bd5bd1f5dd4 100644 --- a/heartbeat/monitors/active/http/http_test.go +++ b/heartbeat/monitors/active/http/http_test.go @@ -38,7 +38,6 @@ import ( "github.com/elastic/beats/libbeat/common/file" "github.com/elastic/beats/libbeat/common/mapval" btesting "github.com/elastic/beats/libbeat/testing" - "github.com/elastic/beats/libbeat/testing/mapvaltest" ) func testRequest(t *testing.T, testURL string) *beat.Event { @@ -192,7 +191,7 @@ func TestUpStatuses(t *testing.T) { t.Run(fmt.Sprintf("Test OK HTTP status %d", status), func(t *testing.T) { server, event := checkServer(t, hbtest.HelloWorldHandler(status)) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.BaseChecks("127.0.0.1", "up", "http"), @@ -212,7 +211,7 @@ func TestDownStatuses(t *testing.T) { t.Run(fmt.Sprintf("test down status %d", status), func(t *testing.T) { server, event := checkServer(t, hbtest.HelloWorldHandler(status)) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.BaseChecks("127.0.0.1", "down", "http"), @@ -249,7 +248,7 @@ func TestLargeResponse(t *testing.T) { _, err = job(event) require.NoError(t, err) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.BaseChecks("127.0.0.1", "up", "http"), @@ -282,7 +281,7 @@ func runHTTPSServerCheck( event := testTLSRequest(t, server.URL, mergedExtraConfig) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.BaseChecks("127.0.0.1", "up", "http"), @@ -348,7 +347,7 @@ func TestConnRefusedJob(t *testing.T) { event := testRequest(t, url) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.BaseChecks(ip, "down", "http"), @@ -370,7 +369,7 @@ func TestUnreachableJob(t *testing.T) { event := testRequest(t, url) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.BaseChecks(ip, "down", "http"), diff --git a/heartbeat/monitors/active/tcp/tcp_test.go b/heartbeat/monitors/active/tcp/tcp_test.go index 36c6c79f3de..fc857a298b5 100644 --- a/heartbeat/monitors/active/tcp/tcp_test.go +++ b/heartbeat/monitors/active/tcp/tcp_test.go @@ -35,7 +35,6 @@ import ( "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/common/mapval" btesting "github.com/elastic/beats/libbeat/testing" - "github.com/elastic/beats/libbeat/testing/mapvaltest" ) func testTCPCheck(t *testing.T, host string, port uint16) *beat.Event { @@ -102,7 +101,7 @@ func TestUpEndpointJob(t *testing.T) { event := testTCPCheck(t, "localhost", port) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.BaseChecks("127.0.0.1", "up", "tcp"), @@ -145,7 +144,7 @@ func TestTLSConnection(t *testing.T) { defer os.Remove(certFile.Name()) event := testTLSTCPCheck(t, ip, port, certFile.Name()) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( hbtest.TLSChecks(0, 0, cert), @@ -166,7 +165,7 @@ func TestConnectionRefusedEndpointJob(t *testing.T) { event := testTCPCheck(t, ip, port) dialErr := fmt.Sprintf("dial tcp %s:%d", ip, port) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( tcpMonitorChecks(ip, ip, port, "down"), @@ -184,7 +183,7 @@ func TestUnreachableEndpointJob(t *testing.T) { event := testTCPCheck(t, ip, port) dialErr := fmt.Sprintf("dial tcp %s:%d", ip, port) - mapvaltest.Test( + mapval.Test( t, mapval.Strict(mapval.Compose( tcpMonitorChecks(ip, ip, port, "down"), diff --git a/heartbeat/monitors/jobs/job.go b/heartbeat/monitors/jobs/job.go index 4c9b9db43f5..efa38739401 100644 --- a/heartbeat/monitors/jobs/job.go +++ b/heartbeat/monitors/jobs/job.go @@ -17,7 +17,9 @@ package jobs -import "github.com/elastic/beats/libbeat/beat" +import ( + "github.com/elastic/beats/libbeat/beat" +) // A Job represents a unit of execution, and may return multiple continuation jobs. type Job func(event *beat.Event) ([]Job, error) @@ -46,6 +48,24 @@ func WrapAll(jobs []Job, wrappers ...JobWrapper) []Job { return wrapped } +// JobWrapperFactory can be used to created new instances of JobWrappers. +type JobWrapperFactory func() JobWrapper + +// WrapAllSeparately wraps the given jobs using the given JobWrapperFactory instances. +// This enables us to use a different JobWrapper for the jobs passed in, but recursively apply +// the same wrapper to their children. +func WrapAllSeparately(jobs []Job, factories ...JobWrapperFactory) []Job { + var wrapped []Job + for _, j := range jobs { + for _, factory := range factories { + wrapper := factory() + j = Wrap(j, wrapper) + } + wrapped = append(wrapped, j) + } + return wrapped +} + // Wrap wraps the given Job and also any continuations with the given JobWrapper. func Wrap(job Job, wrapper JobWrapper) Job { return func(event *beat.Event) ([]Job, error) { diff --git a/heartbeat/monitors/jobs/job_test.go b/heartbeat/monitors/jobs/job_test.go index c26077ba28d..6a3e06223a0 100644 --- a/heartbeat/monitors/jobs/job_test.go +++ b/heartbeat/monitors/jobs/job_test.go @@ -26,7 +26,6 @@ import ( "github.com/elastic/beats/libbeat/beat" "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/common/mapval" - "github.com/elastic/beats/libbeat/testing/mapvaltest" ) func TestWrapAll(t *testing.T) { @@ -117,7 +116,7 @@ func TestWrapAll(t *testing.T) { fr := results[idx].Fields validator := mapval.Strict(mapval.MustCompile(rf)) - mapvaltest.Test(t, validator, fr) + mapval.Test(t, validator, fr) } }) } diff --git a/heartbeat/monitors/monitor_test.go b/heartbeat/monitors/monitor_test.go index bc459278e71..ce86bb16371 100644 --- a/heartbeat/monitors/monitor_test.go +++ b/heartbeat/monitors/monitor_test.go @@ -21,11 +21,12 @@ import ( "testing" "time" + "github.com/elastic/beats/libbeat/common/mapval" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/elastic/beats/heartbeat/scheduler" - "github.com/elastic/beats/libbeat/testing/mapvaltest" ) func TestMonitor(t *testing.T) { @@ -58,7 +59,7 @@ func TestMonitor(t *testing.T) { pcClient.Close() for _, event := range pcClient.Publishes() { - mapvaltest.Test(t, mockEventMonitorValidator(""), event.Fields) + mapval.Test(t, mockEventMonitorValidator(""), event.Fields) } } else { // Let's yield this goroutine so we don't spin diff --git a/heartbeat/monitors/wrappers/monitors.go b/heartbeat/monitors/wrappers/monitors.go index 3d67f36808b..ad05d22c5e9 100644 --- a/heartbeat/monitors/wrappers/monitors.go +++ b/heartbeat/monitors/wrappers/monitors.go @@ -23,38 +23,61 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/mitchellh/hashstructure" + "github.com/pkg/errors" "github.com/elastic/beats/heartbeat/eventext" "github.com/elastic/beats/heartbeat/look" "github.com/elastic/beats/heartbeat/monitors/jobs" "github.com/elastic/beats/libbeat/beat" "github.com/elastic/beats/libbeat/common" + "github.com/elastic/beats/libbeat/logp" ) // WrapCommon applies the common wrappers that all monitor jobs get. func WrapCommon(js []jobs.Job, id string, name string, typ string) []jobs.Job { - return jobs.WrapAll( - js, - addMonitorStatus, - addMonitorDuration, - addMonitorMeta(id, name, typ), - makeAddSummary(id, uint16(len(js))), - ) + return jobs.WrapAllSeparately( + jobs.WrapAll( + js, + addMonitorStatus, + addMonitorDuration, + ), func() jobs.JobWrapper { + return addMonitorMeta(id, name, typ, len(js) > 1) + }, func() jobs.JobWrapper { + return makeAddSummary() + }) } // addMonitorMeta adds the id, name, and type fields to the monitor. -func addMonitorMeta(id string, name string, typ string) jobs.JobWrapper { +func addMonitorMeta(id string, name string, typ string, isMulti bool) jobs.JobWrapper { return func(job jobs.Job) jobs.Job { - return WithFields( - common.MapStr{ - "monitor": common.MapStr{ - "id": id, - "name": name, - "type": typ, + return func(event *beat.Event) ([]jobs.Job, error) { + cont, e := job(event) + thisID := id + + if isMulti { + url, err := event.GetValue("url.full") + if err != nil { + logp.Error(errors.Wrap(err, "Mandatory url.full key missing!")) + url = "n/a" + } + urlHash, _ := hashstructure.Hash(url, nil) + thisID = fmt.Sprintf("%s-%x", id, urlHash) + } + + eventext.MergeEventFields( + event, + common.MapStr{ + "monitor": common.MapStr{ + "id": thisID, + "name": name, + "type": typ, + }, }, - }, - job, - ) + ) + + return cont, e + } } } @@ -99,7 +122,7 @@ func addMonitorDuration(job jobs.Job) jobs.Job { } // makeAddSummary summarizes the job, adding the `summary` field to the last event emitted. -func makeAddSummary(id string, numJobs uint16) jobs.JobWrapper { +func makeAddSummary() jobs.JobWrapper { // This is a tricky method. The way this works is that we track the state across jobs in the // state struct here. state := struct { @@ -114,7 +137,7 @@ func makeAddSummary(id string, numJobs uint16) jobs.JobWrapper { } // Note this is not threadsafe, must be called from a mutex resetState := func() { - state.remaining = numJobs + state.remaining = 1 state.up = 0 state.down = 0 state.generation++ diff --git a/heartbeat/monitors/wrappers/monitors_test.go b/heartbeat/monitors/wrappers/monitors_test.go index b6a150d2103..00755b4c6e2 100644 --- a/heartbeat/monitors/wrappers/monitors_test.go +++ b/heartbeat/monitors/wrappers/monitors_test.go @@ -19,152 +19,213 @@ package wrappers import ( "fmt" + "net/url" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/elastic/beats/heartbeat/eventext" "github.com/elastic/beats/heartbeat/monitors/jobs" "github.com/elastic/beats/libbeat/beat" "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/common/mapval" - "github.com/elastic/beats/libbeat/testing/mapvaltest" ) -func TestWrapCommon(t *testing.T) { - var simpleJob jobs.Job = func(event *beat.Event) ([]jobs.Job, error) { - eventext.MergeEventFields(event, common.MapStr{"simple": "job"}) - return nil, nil - } +type fields struct { + id string + name string + typ string +} - var errorJob jobs.Job = func(event *beat.Event) ([]jobs.Job, error) { - return nil, fmt.Errorf("myerror") - } +type testDef struct { + name string + fields fields + jobs []jobs.Job + want []mapval.Validator +} - var contJob jobs.Job = func(event *beat.Event) ([]jobs.Job, error) { - eventext.MergeEventFields(event, common.MapStr{"cont": "1st"}) - return []jobs.Job{ - func(event *beat.Event) ([]jobs.Job, error) { - eventext.MergeEventFields(event, common.MapStr{"cont": "2nd"}) - return nil, nil - }, - }, nil - } +func testCommonWrap(t *testing.T, tt testDef) { + t.Run(tt.name, func(t *testing.T) { + wrapped := WrapCommon(tt.jobs, tt.fields.id, tt.fields.name, tt.fields.typ) + + results, err := jobs.ExecJobsAndConts(t, wrapped) + assert.NoError(t, err) - type fields struct { - id string - name string - typ string + for idx, r := range results { + t.Run(fmt.Sprintf("result at index %d", idx), func(t *testing.T) { + mapval.Test(t, mapval.Strict(tt.want[idx]), r.Fields) + }) + } + }) +} + +func TestSimpleJob(t *testing.T) { + fields := fields{"myid", "myname", "mytyp"} + testCommonWrap(t, testDef{ + "simple", + fields, + []jobs.Job{makeURLJob(t, "tcp://foo.com:80")}, + []mapval.Validator{ + mapval.Compose( + urlValidator(t, "tcp://foo.com:80"), + mapval.MustCompile(mapval.Map{ + "monitor": mapval.Map{ + "duration.us": mapval.IsDuration, + "id": fields.id, + "name": fields.name, + "type": fields.typ, + "status": "up", + "check_group": mapval.IsString, + }, + }), + summaryValidator(1, 0), + )}, + }) +} + +func TestErrorJob(t *testing.T) { + fields := fields{"myid", "myname", "mytyp"} + + errorJob := func(event *beat.Event) ([]jobs.Job, error) { + return nil, fmt.Errorf("myerror") } - testFields := fields{"myid", "myname", "mytyp"} - commonFieldsValidator := func(f fields, status string) mapval.Validator { - return mapval.MustCompile(mapval.Map{ + errorJobValidator := mapval.Compose( + mapval.MustCompile(mapval.Map{"error": mapval.Map{"message": "myerror", "type": "io"}}), + mapval.MustCompile(mapval.Map{ "monitor": mapval.Map{ "duration.us": mapval.IsDuration, - "id": f.id, - "name": f.name, - "type": f.typ, - "status": status, + "id": fields.id, + "name": fields.name, + "type": fields.typ, + "status": "down", "check_group": mapval.IsString, }, - }) - } + }), + ) - // This duplicates hbtest.SummaryChecks to avoid an import cycle. - // It could be refactored out, but it just isn't worth it. - summaryValidator := func(up int, down int) mapval.Validator { - return mapval.MustCompile(mapval.Map{ - "summary": mapval.Map{ - "up": uint16(up), - "down": uint16(down), - }, - }) + testCommonWrap(t, testDef{ + "job error", + fields, + []jobs.Job{errorJob}, + []mapval.Validator{ + mapval.Compose( + errorJobValidator, + summaryValidator(0, 1), + )}, + }) +} + +func TestMultiJobNoConts(t *testing.T) { + fields := fields{"myid", "myname", "mytyp"} + + uniqScope := mapval.ScopedIsUnique() + + validator := func(u string) mapval.Validator { + return mapval.Compose( + urlValidator(t, u), + mapval.MustCompile(mapval.Map{ + "monitor": mapval.Map{ + "duration.us": mapval.IsDuration, + "id": uniqScope.IsUniqueTo("id"), + "name": fields.name, + "type": fields.typ, + "status": "up", + "check_group": uniqScope.IsUniqueTo("check_group"), + }, + }), + summaryValidator(1, 0), + ) } - simpleJobValidator := mapval.Compose( - mapval.MustCompile(mapval.Map{"simple": "job"}), - commonFieldsValidator(testFields, "up"), - ) + testCommonWrap(t, testDef{ + "multi-job", + fields, + []jobs.Job{makeURLJob(t, "http://foo.com"), makeURLJob(t, "http://bar.com")}, + []mapval.Validator{validator("http://foo.com"), validator("http://bar.com")}, + }) +} - errorJobValidator := mapval.Compose( - mapval.MustCompile(mapval.Map{"error": mapval.Map{"message": "myerror", "type": "io"}}), - commonFieldsValidator(testFields, "down"), - ) +func TestMultiJobConts(t *testing.T) { + fields := fields{"myid", "myname", "mytyp"} + + uniqScope := mapval.ScopedIsUnique() + + makeContJob := func(t *testing.T, u string) jobs.Job { + return func(event *beat.Event) ([]jobs.Job, error) { + eventext.MergeEventFields(event, common.MapStr{"cont": "1st"}) + u, err := url.Parse(u) + require.NoError(t, err) + eventext.MergeEventFields(event, common.MapStr{"url": URLFields(u)}) + return []jobs.Job{ + func(event *beat.Event) ([]jobs.Job, error) { + eventext.MergeEventFields(event, common.MapStr{"cont": "2nd"}) + eventext.MergeEventFields(event, common.MapStr{"url": URLFields(u)}) + return nil, nil + }, + }, nil + } + } - contJobValidator := func(msg string) mapval.Validator { + contJobValidator := func(u string, msg string) mapval.Validator { return mapval.Compose( + urlValidator(t, u), mapval.MustCompile(mapval.Map{"cont": msg}), - commonFieldsValidator(testFields, "up"), + mapval.MustCompile(mapval.Map{ + "monitor": mapval.Map{ + "duration.us": mapval.IsDuration, + "id": uniqScope.IsUniqueTo(u), + "name": fields.name, + "type": fields.typ, + "status": "up", + "check_group": uniqScope.IsUniqueTo(u), + }, + }), ) } - tests := []struct { - name string - fields fields - jobs []jobs.Job - want []mapval.Validator - }{ - { - "simple", - testFields, - []jobs.Job{simpleJob}, - []mapval.Validator{ - mapval.Compose( - simpleJobValidator, - summaryValidator(1, 0), - )}, - }, - { - "job error", - testFields, - []jobs.Job{errorJob}, - []mapval.Validator{ - mapval.Compose( - errorJobValidator, - summaryValidator(0, 1), - )}, - }, - { - "multi-job", - testFields, - []jobs.Job{simpleJob, simpleJob}, - []mapval.Validator{ - simpleJobValidator, - mapval.Compose( - simpleJobValidator, - summaryValidator(2, 0), - ), - }, + testCommonWrap(t, testDef{ + "multi-job-continuations", + fields, + []jobs.Job{makeContJob(t, "http://foo.com"), makeContJob(t, "http://bar.com")}, + []mapval.Validator{ + contJobValidator("http://foo.com", "1st"), + mapval.Compose( + contJobValidator("http://foo.com", "2nd"), + summaryValidator(2, 0), + ), + contJobValidator("http://bar.com", "1st"), + mapval.Compose( + contJobValidator("http://bar.com", "2nd"), + summaryValidator(2, 0), + ), }, - { - "multi-job-continuations", - testFields, - []jobs.Job{contJob, contJob}, - []mapval.Validator{ - contJobValidator("1st"), - contJobValidator("2nd"), - contJobValidator("1st"), - mapval.Compose( - contJobValidator("2nd"), - commonFieldsValidator(testFields, "up"), - summaryValidator(4, 0), - ), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - wrapped := WrapCommon(tt.jobs, tt.fields.id, tt.fields.name, tt.fields.typ) - - results, err := jobs.ExecJobsAndConts(t, wrapped) - assert.NoError(t, err) - - for idx, r := range results { - t.Run(fmt.Sprintf("result at index %d", idx), func(t *testing.T) { - mapvaltest.Test(t, mapval.Strict(tt.want[idx]), r.Fields) - }) - } - }) + }) +} + +func makeURLJob(t *testing.T, u string) jobs.Job { + parsed, err := url.Parse(u) + require.NoError(t, err) + return func(event *beat.Event) (i []jobs.Job, e error) { + eventext.MergeEventFields(event, common.MapStr{"url": URLFields(parsed)}) + return nil, nil } } + +func urlValidator(t *testing.T, u string) mapval.Validator { + parsed, err := url.Parse(u) + require.NoError(t, err) + return mapval.MustCompile(mapval.Map{"url": mapval.Map(URLFields(parsed))}) +} + +// This duplicates hbtest.SummaryChecks to avoid an import cycle. +// It could be refactored out, but it just isn't worth it. +func summaryValidator(up int, down int) mapval.Validator { + return mapval.MustCompile(mapval.Map{ + "summary": mapval.Map{ + "up": uint16(up), + "down": uint16(down), + }, + }) +} diff --git a/heartbeat/monitors/wrappers/util_test.go b/heartbeat/monitors/wrappers/util_test.go index 573ab1dfebe..ecb1bbeffff 100644 --- a/heartbeat/monitors/wrappers/util_test.go +++ b/heartbeat/monitors/wrappers/util_test.go @@ -25,7 +25,6 @@ import ( "github.com/elastic/beats/libbeat/common" "github.com/elastic/beats/libbeat/common/mapval" - "github.com/elastic/beats/libbeat/testing/mapvaltest" ) func TestURLFields(t *testing.T) { @@ -82,7 +81,7 @@ func TestURLFields(t *testing.T) { require.NoError(t, err) got := URLFields(parsed) - mapvaltest.Test(t, mapval.MustCompile(mapval.Map(tt.want)), got) + mapval.Test(t, mapval.MustCompile(mapval.Map(tt.want)), got) }) } } diff --git a/libbeat/common/mapval/is_defs.go b/libbeat/common/mapval/is_defs.go index d85369d4c95..457e85343dc 100644 --- a/libbeat/common/mapval/is_defs.go +++ b/libbeat/common/mapval/is_defs.go @@ -177,6 +177,34 @@ func IsAny(of ...IsDef) IsDef { }) } +// IsUnique instances are used in multiple spots, flagging a value as being in error if it's seen across invocations. +// To use it, assign IsUnique to a variable, then use that variable multiple times in a Map. +func IsUnique() IsDef { + return ScopedIsUnique().IsUniqueTo("") +} + +// UniqScopeTracker is represents the tracking data for invoking IsUniqueTo. +type UniqScopeTracker map[interface{}]string + +// IsUniqueTo validates that the given value is only ever seen within a single namespace. +func (ust UniqScopeTracker) IsUniqueTo(namespace string) IsDef { + return Is("unique", func(path path, v interface{}) *Results { + for trackerK, trackerNs := range ust { + hasNamespace := len(namespace) > 0 + if reflect.DeepEqual(trackerK, v) && (!hasNamespace || namespace != trackerNs) { + return SimpleResult(path, false, "Value '%v' is repeated", v) + } + } + + ust[v] = namespace + return ValidResult(path) + }) +} + +func ScopedIsUnique() UniqScopeTracker { + return UniqScopeTracker{} +} + // isStrCheck is a helper for IsDefs that must assert that the value is a string first. func isStrCheck(path path, v interface{}) (str string, errorResults *Results) { strV, ok := v.(string) diff --git a/libbeat/common/mapval/is_defs_test.go b/libbeat/common/mapval/is_defs_test.go index a6f0e6dcaaf..f50250595fd 100644 --- a/libbeat/common/mapval/is_defs_test.go +++ b/libbeat/common/mapval/is_defs_test.go @@ -23,6 +23,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/elastic/beats/libbeat/common" ) @@ -152,3 +153,58 @@ func TestIsNil(t *testing.T) { assertIsDefValid(t, IsNil, nil) assertIsDefInvalid(t, IsNil, "foo") } + +func TestIsUnique(t *testing.T) { + tests := []struct { + name string + validator func() Validator + data common.MapStr + isValid bool + }{ + { + "IsUnique find dupes", + func() Validator { + v := IsUnique() + return MustCompile(Map{"a": v, "b": v}) + }, + common.MapStr{"a": 1, "b": 1}, + false, + }, + { + "IsUnique separate instances don't care about dupes", + func() Validator { return MustCompile(Map{"a": IsUnique(), "b": IsUnique()}) }, + common.MapStr{"a": 1, "b": 1}, + true, + }, + { + "IsUniqueTo duplicates across namespaces fail", + func() Validator { + s := ScopedIsUnique() + return MustCompile(Map{"a": s.IsUniqueTo("test"), "b": s.IsUniqueTo("test2")}) + }, + common.MapStr{"a": 1, "b": 1}, + false, + }, + + { + "IsUniqueTo duplicates within a namespace succeeds", + func() Validator { + s := ScopedIsUnique() + return MustCompile(Map{"a": s.IsUniqueTo("test"), "b": s.IsUniqueTo("test")}) + }, + common.MapStr{"a": 1, "b": 1}, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + if tt.isValid { + Test(t, tt.validator(), tt.data) + } else { + result := tt.validator()(tt.data) + require.False(t, result.Valid) + } + }) + } +} diff --git a/libbeat/testing/mapvaltest/mapvaltest.go b/libbeat/common/mapval/testing.go similarity index 79% rename from libbeat/testing/mapvaltest/mapvaltest.go rename to libbeat/common/mapval/testing.go index 0701d4c61d5..e318b5d5c8a 100644 --- a/libbeat/testing/mapvaltest/mapvaltest.go +++ b/libbeat/common/mapval/testing.go @@ -15,11 +15,7 @@ // specific language governing permissions and limitations // under the License. -package mapvaltest - -// skimatest is a separate package from skima since we don't want to import "testing" -// into skima, since there is a good chance we'll use skima for running user-defined -// tests in heartbeat at runtime. +package mapval import ( "testing" @@ -28,12 +24,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/elastic/beats/libbeat/common" - "github.com/elastic/beats/libbeat/common/mapval" ) // Test takes the output from a Validator invocation and runs test assertions on the result. // If you are using this library for testing you will probably want to run Test(t, Compile(Map{...}), actual) as a pattern. -func Test(t *testing.T, v mapval.Validator, m common.MapStr) *mapval.Results { +func Test(t *testing.T, v Validator, m common.MapStr) *Results { r := v(m) if !r.Valid { diff --git a/libbeat/testing/mapvaltest/mapvaltest_test.go b/libbeat/testing/mapvaltest/mapvaltest_test.go deleted file mode 100644 index 9f22e28f3dd..00000000000 --- a/libbeat/testing/mapvaltest/mapvaltest_test.go +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you 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 mapvaltest - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/elastic/beats/libbeat/common" - "github.com/elastic/beats/libbeat/common/mapval" -) - -func TestTest(t *testing.T) { - // Should pass - Test(t, mapval.MustCompile(mapval.Map{}), common.MapStr{}) - - fakeT := new(testing.T) - Test(fakeT, mapval.MustCompile(mapval.Map{"foo": "bar"}), common.MapStr{}) - - assert.True(t, fakeT.Failed()) -}