diff --git a/cache.go b/cache.go index af409a3..870eccb 100644 --- a/cache.go +++ b/cache.go @@ -77,28 +77,50 @@ type Config struct { // to the MaxAge ExpirationInterval time.Duration // Optional callback invoked when an item is evicted due to the LRU policy - OnEviction func(key, value interface{}) + OnEviction func(key interface{}, value interface{}) // Optional callback invoked when an item expired - OnExpiration func(key, value interface{}) + OnExpiration func(key interface{}, value interface{}) +} + +// ConfigGeneric configures the cache. +type ConfigGeneric[T any] struct { + // Maximum number of items in the cache + Capacity int + // Optional max duration before an item expires. Must be greater than or + // equal to MinAge. If zero, expiration is disabled. + MaxAge time.Duration + // Optional min duration before an item expires. Must be less than or equal + // to MaxAge. When less than MaxAge, uniformly distributed random jitter is + // added to the expiration time. If equal or zero, jitter is disabled. + MinAge time.Duration + // Type of key expiration: Passive or Active + ExpirationType ExpirationType + // For active expiration, how often to iterate over the keyspace. Defaults + // to the MaxAge + ExpirationInterval time.Duration + // Optional callback invoked when an item is evicted due to the LRU policy + OnEviction func(key interface{}, value T) + // Optional callback invoked when an item expired + OnExpiration func(key interface{}, value T) } // Entry pointed to by each list.Element -type cacheEntry struct { +type cacheEntry[T any] struct { key interface{} - value interface{} + value T timestamp time.Time } // Cache implements a thread-safe fixed-capacity LRU cache. -type Cache struct { +type Cache[T any] struct { // Fields defined by configuration capacity int minAge time.Duration maxAge time.Duration expirationType ExpirationType expirationInterval time.Duration - onEviction func(key, value interface{}) - onExpiration func(key, value interface{}) + onEviction func(key interface{}, value T) + onExpiration func(key interface{}, value T) // Cache statistics sets int64 @@ -117,7 +139,23 @@ type Cache struct { // must be a positive int, and config.MaxAge a zero or positive duration. A // duration of zero disables item expiration. Panics given an invalid // config.Capacity or config.MaxAge. -func New(config Config) *Cache { +func New(config Config) *Cache[interface{}] { + return NewGeneric(ConfigGeneric[interface{}]{ + Capacity: config.Capacity, + MaxAge: config.MaxAge, + MinAge: config.MinAge, + ExpirationType: config.ExpirationType, + ExpirationInterval: config.ExpirationInterval, + OnEviction: config.OnEviction, + OnExpiration: config.OnExpiration, + }) +} + +// NewGeneric constructs an LRU Cache with the given Config object. config.Capacity +// must be a positive int, and config.MaxAge a zero or positive duration. A +// duration of zero disables item expiration. Panics given an invalid +// config.Capacity or config.MaxAge. +func NewGeneric[T any](config ConfigGeneric[T]) *Cache[T] { if config.Capacity <= 0 { panic("Must supply a positive config.Capacity") } @@ -146,7 +184,7 @@ func New(config Config) *Cache { seed := rand.NewSource(time.Now().UnixNano()) - cache := &Cache{ + cache := &Cache[T]{ capacity: config.Capacity, maxAge: config.MaxAge, minAge: minAge, @@ -172,7 +210,8 @@ func New(config Config) *Cache { // Set updates a key:value pair in the cache. Returns true if an eviction // occurrred, and subsequently invokes the OnEviction callback. -func (cache *Cache) Set(key, value interface{}) bool { +func (cache *Cache[T]) Set(key interface{}, value T) bool { + cache.mutex.Lock() defer cache.mutex.Unlock() @@ -181,13 +220,13 @@ func (cache *Cache) Set(key, value interface{}) bool { if element, ok := cache.items[key]; ok { cache.evictionList.MoveToFront(element) - entry := element.Value.(*cacheEntry) + entry := element.Value.(*cacheEntry[T]) entry.value = value entry.timestamp = timestamp return false } - entry := &cacheEntry{key, value, timestamp} + entry := &cacheEntry[T]{key, value, timestamp} element := cache.evictionList.PushFront(entry) cache.items[key] = element @@ -201,18 +240,18 @@ func (cache *Cache) Set(key, value interface{}) bool { // Get returns the value stored at `key`. The boolean value reports whether or // not the value was found. The OnExpiration callback is invoked if the value // had expired on access -func (cache *Cache) Get(key interface{}) (interface{}, bool) { +func (cache *Cache[T]) Get(key interface{}) (*T, bool) { cache.mutex.Lock() defer cache.mutex.Unlock() cache.gets++ if element, ok := cache.items[key]; ok { - entry := element.Value.(*cacheEntry) + entry := element.Value.(*cacheEntry[T]) if cache.maxAge == 0 || time.Since(entry.timestamp) <= cache.maxAge { cache.evictionList.MoveToFront(element) cache.hits++ - return entry.value, true + return &entry.value, true } // Entry expired @@ -230,7 +269,7 @@ func (cache *Cache) Get(key interface{}) (interface{}, bool) { // Has returns whether or not the `key` is in the cache without updating // how recently it was accessed or deleting it for having expired. -func (cache *Cache) Has(key interface{}) bool { +func (cache *Cache[T]) Has(key interface{}) bool { cache.mutex.RLock() defer cache.mutex.RUnlock() @@ -241,12 +280,12 @@ func (cache *Cache) Has(key interface{}) bool { // Peek returns the value at the specified key and a boolean specifying whether // or not it was found, without updating how recently it was accessed or // deleting it for having expired. -func (cache *Cache) Peek(key interface{}) (interface{}, bool) { +func (cache *Cache[T]) Peek(key interface{}) (*T, bool) { cache.mutex.RLock() defer cache.mutex.RUnlock() if element, ok := cache.items[key]; ok { - return element.Value.(*cacheEntry).value, true + return &element.Value.(*cacheEntry[T]).value, true } return nil, false @@ -254,7 +293,7 @@ func (cache *Cache) Peek(key interface{}) (interface{}, bool) { // Remove removes the provided key from the cache, returning a bool indicating // whether or not it existed. -func (cache *Cache) Remove(key interface{}) bool { +func (cache *Cache[T]) Remove(key interface{}) bool { cache.mutex.Lock() defer cache.mutex.Unlock() @@ -269,7 +308,7 @@ func (cache *Cache) Remove(key interface{}) bool { // EvictOldest removes the oldest item from the cache, while also invoking any // eviction callback. A bool is returned indicating whether or not an item was // removed -func (cache *Cache) EvictOldest() bool { +func (cache *Cache[T]) EvictOldest() bool { cache.mutex.Lock() defer cache.mutex.Unlock() @@ -277,7 +316,7 @@ func (cache *Cache) EvictOldest() bool { } // Len returns the number of items in the cache. -func (cache *Cache) Len() int { +func (cache *Cache[T]) Len() int { cache.mutex.RLock() defer cache.mutex.RUnlock() @@ -285,7 +324,7 @@ func (cache *Cache) Len() int { } // Clear empties the cache. -func (cache *Cache) Clear() { +func (cache *Cache[T]) Clear() { cache.mutex.Lock() defer cache.mutex.Unlock() @@ -296,7 +335,7 @@ func (cache *Cache) Clear() { } // Keys returns all keys in the cache. -func (cache *Cache) Keys() []interface{} { +func (cache *Cache[T]) Keys() []interface{} { cache.mutex.RLock() defer cache.mutex.RUnlock() @@ -312,7 +351,7 @@ func (cache *Cache) Keys() []interface{} { } // OrderedKeys returns all keys in the cache, ordered from oldest to newest. -func (cache *Cache) OrderedKeys() []interface{} { +func (cache *Cache[T]) OrderedKeys() []interface{} { cache.mutex.RLock() defer cache.mutex.RUnlock() @@ -320,7 +359,7 @@ func (cache *Cache) OrderedKeys() []interface{} { i := 0 for element := cache.evictionList.Back(); element != nil; element = element.Prev() { - keys[i] = element.Value.(*cacheEntry).key + keys[i] = element.Value.(*cacheEntry[T]).key i++ } @@ -330,7 +369,7 @@ func (cache *Cache) OrderedKeys() []interface{} { // SetMaxAge updates the max age for items in the cache. A duration of zero // disables expiration. A negative duration, or one that is less than minAge, // results in an error. -func (cache *Cache) SetMaxAge(maxAge time.Duration) error { +func (cache *Cache[T]) SetMaxAge(maxAge time.Duration) error { if maxAge < 0 { return errors.New("Must supply a zero or positive maxAge") } else if maxAge < cache.minAge { @@ -348,7 +387,7 @@ func (cache *Cache) SetMaxAge(maxAge time.Duration) error { // SetMinAge updates the min age for items in the cache. A duration of zero // or equal to maxAge disables jitter. A negative duration, or one that is // greater than maxAge, results in an error. -func (cache *Cache) SetMinAge(minAge time.Duration) error { +func (cache *Cache[T]) SetMinAge(minAge time.Duration) error { if minAge < 0 { return errors.New("Must supply a zero or positive minAge") } else if minAge > cache.maxAge { @@ -368,7 +407,7 @@ func (cache *Cache) SetMinAge(minAge time.Duration) error { } // OnEviction sets the eviction callback. -func (cache *Cache) OnEviction(callback func(key, value interface{})) { +func (cache *Cache[T]) OnEviction(callback func(key interface{}, value T)) { cache.mutex.Lock() defer cache.mutex.Unlock() @@ -376,7 +415,7 @@ func (cache *Cache) OnEviction(callback func(key, value interface{})) { } // OnExpiration sets the expiration callback. -func (cache *Cache) OnExpiration(callback func(key, value interface{})) { +func (cache *Cache[T]) OnExpiration(callback func(key interface{}, value T)) { cache.mutex.Lock() defer cache.mutex.Unlock() @@ -384,7 +423,7 @@ func (cache *Cache) OnExpiration(callback func(key, value interface{})) { } // Stats returns cache stats. -func (cache *Cache) Stats() Stats { +func (cache *Cache[T]) Stats() Stats { cache.mutex.RLock() defer cache.mutex.RUnlock() @@ -401,7 +440,7 @@ func (cache *Cache) Stats() Stats { // Resize the cache to hold at most n entries. If n is smaller than the current // size, entries are evicted to fit the new size. It errors if n <= 0. -func (cache *Cache) Resize(n int) error { +func (cache *Cache[T]) Resize(n int) error { if n <= 0 { return errors.New("must supply a positive capacity to Resize") } @@ -421,14 +460,14 @@ func (cache *Cache) Resize(n int) error { return nil } -func (cache *Cache) deleteExpired() { +func (cache *Cache[T]) deleteExpired() { keys := cache.Keys() for i := range keys { cache.mutex.Lock() if element, ok := cache.items[keys[i]]; ok { - entry := element.Value.(*cacheEntry) + entry := element.Value.(*cacheEntry[T]) if cache.maxAge > 0 && time.Since(entry.timestamp) > cache.maxAge { cache.deleteElement(element) if cache.onExpiration != nil { @@ -441,7 +480,7 @@ func (cache *Cache) deleteExpired() { } } -func (cache *Cache) evictOldest() bool { +func (cache *Cache[T]) evictOldest() bool { element := cache.evictionList.Back() if element == nil { return false @@ -455,14 +494,14 @@ func (cache *Cache) evictOldest() bool { return true } -func (cache *Cache) deleteElement(element *list.Element) *cacheEntry { +func (cache *Cache[T]) deleteElement(element *list.Element) *cacheEntry[T] { cache.evictionList.Remove(element) - entry := element.Value.(*cacheEntry) + entry := element.Value.(*cacheEntry[T]) delete(cache.items, entry.key) return entry } -func (cache *Cache) getTimestamp() time.Time { +func (cache *Cache[T]) getTimestamp() time.Time { timestamp := time.Now() if cache.minAge == cache.maxAge { return timestamp diff --git a/cache_test.go b/cache_test.go index 259395c..738b3c4 100644 --- a/cache_test.go +++ b/cache_test.go @@ -10,23 +10,23 @@ import ( func TestInvalidCapacity(t *testing.T) { assert.Panics(t, func() { - New(Config{Capacity: 0}) + NewGeneric(ConfigGeneric[string]{Capacity: 0}) }) } func TestInvalidMaxAge(t *testing.T) { assert.Panics(t, func() { - New(Config{Capacity: 1, MaxAge: -1 * time.Hour}) + NewGeneric(ConfigGeneric[string]{Capacity: 1, MaxAge: -1 * time.Hour}) }) } func TestInvalidMinAge(t *testing.T) { assert.Panics(t, func() { - New(Config{Capacity: 1, MinAge: -1 * time.Hour}) + NewGeneric(ConfigGeneric[string]{Capacity: 1, MinAge: -1 * time.Hour}) }) assert.Panics(t, func() { - New(Config{ + NewGeneric(ConfigGeneric[string]{ Capacity: 1, MaxAge: time.Hour, MinAge: 2 * time.Hour, @@ -35,36 +35,36 @@ func TestInvalidMinAge(t *testing.T) { } func TestBasicSetGet(t *testing.T) { - cache := New(Config{Capacity: 2}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 2}) cache.Set("foo", 1) cache.Set("bar", 2) val, ok := cache.Get("foo") assert.True(t, ok) - assert.Equal(t, 1, val) + assert.Equal(t, 1, *val) val, ok = cache.Get("bar") assert.True(t, ok) - assert.Equal(t, 2, val) + assert.Equal(t, 2, *val) } func TestBasicSetOverwrite(t *testing.T) { - cache := New(Config{Capacity: 2}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 2}) cache.Set("foo", 1) evict := cache.Set("foo", 2) val, ok := cache.Get("foo") assert.False(t, evict) assert.True(t, ok) - assert.Equal(t, 2, val) + assert.Equal(t, 2, *val) } func TestEviction(t *testing.T) { var k, v interface{} - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[int]{ Capacity: 2, - OnEviction: func(key, value interface{}) { + OnEviction: func(key interface{}, value int) { k = key v = value }, @@ -86,14 +86,14 @@ func TestExpiration(t *testing.T) { var k, v interface{} var eviction bool - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[int]{ Capacity: 1, MaxAge: time.Millisecond, - OnExpiration: func(key, value interface{}) { + OnExpiration: func(key interface{}, value int) { k = key v = value }, - OnEviction: func(key, value interface{}) { + OnEviction: func(key interface{}, value int) { eviction = true }, }) @@ -125,7 +125,7 @@ func (mock *MockRandGenerator) Int63n(n int64) int64 { } func TestJitter(t *testing.T) { - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[string]{ Capacity: 1, MaxAge: 350 * time.Millisecond, MinAge: time.Millisecond, @@ -165,7 +165,7 @@ func TestJitter(t *testing.T) { } func TestHas(t *testing.T) { - cache := New(Config{Capacity: 1, MaxAge: time.Millisecond}) + cache := NewGeneric(ConfigGeneric[string]{Capacity: 1, MaxAge: time.Millisecond}) cache.Set("foo", "bar") <-time.After(time.Millisecond * 2) @@ -174,21 +174,21 @@ func TestHas(t *testing.T) { } func TestPeek(t *testing.T) { - cache := New(Config{Capacity: 1, MaxAge: time.Millisecond}) + cache := NewGeneric(ConfigGeneric[string]{Capacity: 1, MaxAge: time.Millisecond}) cache.Set("foo", "bar") <-time.After(time.Millisecond * 2) val, ok := cache.Peek("foo") assert.True(t, ok) - assert.Equal(t, "bar", val) + assert.Equal(t, "bar", *val) } func TestRemove(t *testing.T) { var eviction bool - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[string]{ Capacity: 1, - OnEviction: func(key, value interface{}) { + OnEviction: func(key interface{}, value string) { eviction = true }, }) @@ -207,9 +207,9 @@ func TestRemove(t *testing.T) { func TestEvictOldest(t *testing.T) { var eviction bool - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[string]{ Capacity: 1, - OnEviction: func(key, value interface{}) { + OnEviction: func(key interface{}, value string) { eviction = true }, }) @@ -231,7 +231,7 @@ func TestEvictOldest(t *testing.T) { } func TestLen(t *testing.T) { - cache := New(Config{Capacity: 10}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 10}) for i := 0; i <= 9; i++ { evict := cache.Set(i, i) assert.False(t, evict) @@ -241,7 +241,7 @@ func TestLen(t *testing.T) { } func TestClear(t *testing.T) { - cache := New(Config{Capacity: 10}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 10}) for i := 0; i <= 9; i++ { evict := cache.Set(i, i) assert.False(t, evict) @@ -257,7 +257,7 @@ func TestClear(t *testing.T) { } func TestKeys(t *testing.T) { - cache := New(Config{Capacity: 10}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 10}) cache.Set("foo", 1) cache.Set("bar", 2) @@ -272,7 +272,7 @@ func TestKeys(t *testing.T) { } func TestOrderedKeys(t *testing.T) { - cache := New(Config{Capacity: 10}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 10}) cache.Set("foo", 1) cache.Set("bar", 2) @@ -284,7 +284,7 @@ func TestOrderedKeys(t *testing.T) { } func TestSetMaxAge(t *testing.T) { - cache := New(Config{Capacity: 10}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 10}) err := cache.SetMaxAge(-1 * time.Hour) assert.Error(t, err) @@ -293,7 +293,7 @@ func TestSetMaxAge(t *testing.T) { } func TestSetMinAge(t *testing.T) { - cache := New(Config{Capacity: 10, MaxAge: time.Hour}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 10, MaxAge: time.Hour}) err := cache.SetMinAge(-1 * time.Hour) assert.Error(t, err) @@ -304,8 +304,8 @@ func TestSetMinAge(t *testing.T) { func TestOnEviction(t *testing.T) { var eviction bool - cache := New(Config{Capacity: 1}) - cache.OnEviction(func(key, value interface{}) { + cache := NewGeneric(ConfigGeneric[int]{Capacity: 1}) + cache.OnEviction(func(key interface{}, value int) { eviction = true }) @@ -318,11 +318,11 @@ func TestOnEviction(t *testing.T) { func TestOnExpiration(t *testing.T) { var expiration bool - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[int]{ Capacity: 1, MaxAge: time.Millisecond, }) - cache.OnExpiration(func(key, value interface{}) { + cache.OnExpiration(func(key interface{}, value int) { expiration = true }) @@ -336,13 +336,13 @@ func TestOnExpiration(t *testing.T) { func TestActiveExpiration(t *testing.T) { invoked := make(chan bool) - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[int]{ Capacity: 1, MaxAge: time.Millisecond, ExpirationType: ActiveExpiration, }) - cache.OnExpiration(func(key, value interface{}) { + cache.OnExpiration(func(key interface{}, value int) { invoked <- true }) @@ -355,7 +355,7 @@ func TestActiveExpiration(t *testing.T) { } func TestResize(t *testing.T) { - cache := New(Config{ + cache := NewGeneric(ConfigGeneric[int]{ Capacity: 2, }) cache.Set("a", 1) @@ -387,12 +387,12 @@ func TestResize(t *testing.T) { func TestStats(t *testing.T) { t.Run("reports capacity", func(t *testing.T) { - cache := New(Config{Capacity: 100}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 100}) assert.Equal(t, int64(100), cache.Stats().Capacity) }) t.Run("reports count", func(t *testing.T) { - cache := New(Config{Capacity: 100}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 100}) for i := 0; i < 10; i++ { cache.Set(i, i) } @@ -400,7 +400,7 @@ func TestStats(t *testing.T) { }) t.Run("increments sets", func(t *testing.T) { - cache := New(Config{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[string]{Capacity: 100, MaxAge: time.Second}) for i := 0; i < 10; i++ { cache.Set("foo", "bar") } @@ -408,7 +408,7 @@ func TestStats(t *testing.T) { }) t.Run("increments gets", func(t *testing.T) { - cache := New(Config{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 100, MaxAge: time.Second}) for i := 0; i < 10; i++ { cache.Get("foo") } @@ -416,20 +416,20 @@ func TestStats(t *testing.T) { }) t.Run("increments hits", func(t *testing.T) { - cache := New(Config{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[string]{Capacity: 100, MaxAge: time.Second}) cache.Set("foo", "bar") cache.Get("foo") assert.Equal(t, int64(1), cache.Stats().Gets) }) t.Run("increments misses", func(t *testing.T) { - cache := New(Config{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 100, MaxAge: time.Second}) cache.Get("foo") assert.Equal(t, int64(1), cache.Stats().Misses) }) t.Run("increments evictions", func(t *testing.T) { - cache := New(Config{Capacity: 1, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 1, MaxAge: time.Second}) for i := 0; i < 10; i++ { cache.Set(i, i) } @@ -437,7 +437,7 @@ func TestStats(t *testing.T) { }) t.Run("delta stats", func(t *testing.T) { - cache := New(Config{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[string]{Capacity: 100, MaxAge: time.Second}) cache.Set("a", "1") prev := cache.Stats() @@ -460,7 +460,7 @@ func TestStats(t *testing.T) { }) t.Run("copy", func(t *testing.T) { - cache := New(Config{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[int]{Capacity: 100, MaxAge: time.Second}) stats := cache.Stats() stats.Hits++ stats.Misses++ @@ -470,7 +470,7 @@ func TestStats(t *testing.T) { } func BenchmarkCache(b *testing.B) { - cache := New(Config{Capacity: 100, MaxAge: time.Second}) + cache := NewGeneric(ConfigGeneric[string]{Capacity: 100, MaxAge: time.Second}) b.RunParallel(func(pb *testing.PB) { for pb.Next() { diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..28af0b0 --- /dev/null +++ b/example_test.go @@ -0,0 +1,20 @@ +package agecache + +import ( + "fmt" + "time" +) + +func ExampleNew() { + // Create a new cache of type string, that expires after 10 mintues + cache := NewGeneric(ConfigGeneric[string]{ + Capacity: 10, + ExpirationInterval: time.Minute * 10, + }) + + cache.Set("key", "value") + value, ok := cache.Get("key") + fmt.Printf("%v: %s\n", ok, *value) + + // Output: true: value +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..41f5c6a --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/segmentio/agecache + +go 1.18 + +require github.com/stretchr/testify v1.7.0 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..acb88a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=