From ba8c1569c7ed0b9bf9f69caef3e6369233b40f5f Mon Sep 17 00:00:00 2001 From: johnbass Date: Thu, 26 Dec 2024 13:49:36 -0800 Subject: [PATCH 1/2] chore: unit tests --- consistent/builder_test.go | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 consistent/builder_test.go diff --git a/consistent/builder_test.go b/consistent/builder_test.go new file mode 100644 index 0000000..785f761 --- /dev/null +++ b/consistent/builder_test.go @@ -0,0 +1,79 @@ +package consistent + +import ( + "hash/fnv" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/xmidt-org/medley" +) + +type BuilderSuite struct { + suite.Suite + + object []byte +} + +func (suite *BuilderSuite) SetupTest() { + suite.object = []byte("test value") +} + +func (suite *BuilderSuite) testStringsDefault() { + services := []string{"service1", "service2", "service3"} + ring := Strings(services...).Build() + suite.Require().NotNil(ring) + + result, err := ring.Find(suite.object) + suite.NoError(err) + suite.Contains(services, result) +} + +func (suite *BuilderSuite) testStringsCustom() { + services := []string{"service1", "service2", "service3", "additional1", "additional2"} + ring := Strings(services[:3]...). + VNodes(100). + Services(services[3:]...). + Algorithm(medley.Algorithm{New64: fnv.New64}). + ServiceHasher(nil). // force the default + Build() + + suite.Require().NotNil(ring) + + result, err := ring.Find(suite.object) + suite.NoError(err) + suite.Contains(services, result) +} + +func (suite *BuilderSuite) TestStrings() { + suite.Run("Default", suite.testStringsDefault) + suite.Run("Custom", suite.testStringsCustom) +} + +func (suite *BuilderSuite) TestServices() { + services := []string{"service1", "service2", "service3"} + ring := Services(services...).Build() + suite.Require().NotNil(ring) + + result, err := ring.Find(suite.object) + suite.NoError(err) + suite.Contains(services, result) +} + +func (suite *BuilderSuite) TestBasicServices() { + services := []medley.BasicService{ + {Host: "service1.net"}, + {Host: "service2.net", Port: 8080}, + {Host: "service3.net", Path: "/foo/bar"}, + } + + ring := BasicServices(services...).Build() + suite.Require().NotNil(ring) + + result, err := ring.Find(suite.object) + suite.NoError(err) + suite.Contains(services, result) +} + +func TestBuilder(t *testing.T) { + suite.Run(t, new(BuilderSuite)) +} From f842432788d0dee09f2e7643fa409cf2a3bee2d8 Mon Sep 17 00:00:00 2001 From: johnbass Date: Thu, 26 Dec 2024 14:51:24 -0800 Subject: [PATCH 2/2] chore: unit tests --- consistent/builder_test.go | 5 ++ consistent/ring_test.go | 125 +++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 consistent/ring_test.go diff --git a/consistent/builder_test.go b/consistent/builder_test.go index 785f761..ff8c56d 100644 --- a/consistent/builder_test.go +++ b/consistent/builder_test.go @@ -2,6 +2,7 @@ package consistent import ( "hash/fnv" + "sort" "testing" "github.com/stretchr/testify/suite" @@ -22,6 +23,7 @@ func (suite *BuilderSuite) testStringsDefault() { services := []string{"service1", "service2", "service3"} ring := Strings(services...).Build() suite.Require().NotNil(ring) + suite.Require().True(sort.IsSorted(ring.nodes)) result, err := ring.Find(suite.object) suite.NoError(err) @@ -38,6 +40,7 @@ func (suite *BuilderSuite) testStringsCustom() { Build() suite.Require().NotNil(ring) + suite.Require().True(sort.IsSorted(ring.nodes)) result, err := ring.Find(suite.object) suite.NoError(err) @@ -53,6 +56,7 @@ func (suite *BuilderSuite) TestServices() { services := []string{"service1", "service2", "service3"} ring := Services(services...).Build() suite.Require().NotNil(ring) + suite.Require().True(sort.IsSorted(ring.nodes)) result, err := ring.Find(suite.object) suite.NoError(err) @@ -68,6 +72,7 @@ func (suite *BuilderSuite) TestBasicServices() { ring := BasicServices(services...).Build() suite.Require().NotNil(ring) + suite.Require().True(sort.IsSorted(ring.nodes)) result, err := ring.Find(suite.object) suite.NoError(err) diff --git a/consistent/ring_test.go b/consistent/ring_test.go new file mode 100644 index 0000000..2a62c03 --- /dev/null +++ b/consistent/ring_test.go @@ -0,0 +1,125 @@ +package consistent + +import ( + "math/rand" + "sort" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/xmidt-org/medley" +) + +const ( + // objectSeed is the random number seed we use to create objects to hash + objectSeed int64 = 7245298734452934458 + + // objectCount is the number of random objects we generate for hash inputs + objectCount int = 1000 +) + +type RingSuite struct { + suite.Suite + + rand *rand.Rand + objects [objectCount][16]byte + + originalServices []string + original *Ring[string] +} + +func (suite *RingSuite) SetupSuite() { + suite.rand = rand.New( + rand.NewSource(objectSeed), + ) + + for i := 0; i < len(suite.objects); i++ { + suite.rand.Read(suite.objects[i][:]) + } + + suite.originalServices = []string{ + "original1.service.net", "original2.service.net", "original3.service.net", "original4.service.net", + } + + suite.original = Strings(suite.originalServices...).Build() + suite.Require().NotNil(suite.original) + suite.Require().True(sort.IsSorted(suite.original.nodes)) + + distribution := make(map[string]int) + for _, object := range suite.objects { + result, err := suite.original.Find(object[:]) + suite.Require().NoError(err) + suite.Require().Contains(suite.originalServices, result) + distribution[result] += 1 + } + + // the distribution should be close to even + expectedCount := objectCount / len(suite.originalServices) + for _, actualCount := range distribution { + // each count should be within 25% of its expected value. + // 25% is just a guess, but it should prevent drift as + // the codebase changes. + suite.InEpsilon(expectedCount, actualCount, 0.25) + } +} + +func (suite *RingSuite) update(services ...string) (*Ring[string], bool) { + updated, didUpdate := Update(suite.original, services...) + suite.Require().NotNil(updated) + suite.Require().True(sort.IsSorted(updated.nodes)) + + return updated, didUpdate +} + +func (suite *RingSuite) testUpdateEmpty() { + // the list of updated services is empty + updated, didUpdate := suite.update() + suite.True(didUpdate) + suite.Empty(updated.nodes) + + for _, object := range suite.objects { + result, err := updated.Find(object[:]) + suite.Empty(result) + suite.ErrorIs(err, medley.ErrNoServices) + } +} + +func (suite *RingSuite) testUpdatePartial() { + partial := []string{"new1", suite.originalServices[0], "new2"} + updated, didUpdate := suite.update(partial...) + suite.True(didUpdate) + + for _, object := range suite.objects { + result, err := updated.Find(object[:]) + suite.Contains(partial, result) + suite.NoError(err) + } +} + +func (suite *RingSuite) testUpdateAllNew() { + allNew := []string{"new1.service.com", "new2.service.com", "new3.service.com", "new4.service.com"} + updated, didUpdate := suite.update(allNew...) + suite.True(didUpdate) + + for _, object := range suite.objects { + result, err := updated.Find(object[:]) + suite.Contains(allNew, result) + suite.NoError(err) + } +} + +func (suite *RingSuite) testUpdateNotNeeded() { + updated, didUpdate := suite.update(suite.originalServices...) + suite.Same(suite.original, updated) + suite.False(didUpdate) +} + +func (suite *RingSuite) TestUpdate() { + suite.Run("Empty", suite.testUpdateEmpty) + suite.Run("Partial", suite.testUpdatePartial) + suite.Run("AllNew", suite.testUpdateAllNew) + suite.Run("NotNeeded", suite.testUpdateNotNeeded) +} + +func TestRing(t *testing.T) { + suite.Run(t, new(RingSuite)) +}