external-dns/provider/blueprint/zone_cache_test.go
Ivan Ka 1756cdd5d1
chore(provider): zone cache provider interface (#6120)
* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

* chore(provider): zone cache provider interface

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>

---------

Signed-off-by: ivan katliarchuk <ivan.katliarchuk@gmail.com>
2026-01-25 14:32:13 +05:30

153 lines
4.1 KiB
Go

/*
Copyright 2026 The Kubernetes Authors.
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 blueprint
import (
"sync"
"sync/atomic"
"testing"
"testing/synctest"
"time"
"github.com/stretchr/testify/assert"
)
func TestZoneCache_SliceCache(t *testing.T) {
cache := NewZoneCache[[]string](time.Hour)
// Initially expired (empty)
assert.True(t, cache.Expired())
// After reset, not expired
cache.Reset([]string{"zone1", "zone2"})
assert.False(t, cache.Expired())
assert.Equal(t, []string{"zone1", "zone2"}, cache.Get())
}
func TestZoneCache_MapCache(t *testing.T) {
cache := NewZoneCache[map[string]int](time.Hour)
// Initially expired (empty)
assert.True(t, cache.Expired())
// After reset, not expired
cache.Reset(map[string]int{"a": 1, "b": 2})
assert.False(t, cache.Expired())
assert.Equal(t, map[string]int{"a": 1, "b": 2}, cache.Get())
}
func TestZoneCache_Expiration(t *testing.T) {
// Very short duration for testing
cache := NewZoneCache[[]string](10 * time.Millisecond)
cache.Reset([]string{"zone1"})
assert.False(t, cache.Expired())
// Wait for expiration
time.Sleep(20 * time.Millisecond)
assert.True(t, cache.Expired())
}
func TestZoneCache_CachingDisabled(t *testing.T) {
cache := NewZoneCache[[]string](0)
cache.Reset([]string{"zone1"})
// Should still be expired because caching is disabled
assert.True(t, cache.Expired())
// Data should not be stored
assert.Nil(t, cache.Get())
}
func TestZoneCache_Expiration_Synctest(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
cache := NewZoneCache[[]string](5 * time.Minute)
cache.Reset([]string{"zone1", "zone2"})
assert.False(t, cache.Expired(), "should not be expired immediately after reset")
assert.Equal(t, []string{"zone1", "zone2"}, cache.Get())
// Advance time but not past expiration
time.Sleep(3 * time.Minute)
assert.False(t, cache.Expired(), "should not be expired before duration")
// Advance time past expiration
time.Sleep(3 * time.Minute) // Total: 6 minutes > 5 minute duration
assert.True(t, cache.Expired(), "should be expired after duration")
// Data is still accessible even when expired
assert.Equal(t, []string{"zone1", "zone2"}, cache.Get())
// Reset refreshes the cache
cache.Reset([]string{"zone3"})
assert.False(t, cache.Expired(), "should not be expired after fresh reset")
assert.Equal(t, []string{"zone3"}, cache.Get())
})
}
func TestZoneCache_ThreadSafety(t *testing.T) {
cache := NewZoneCache[[]int](time.Hour)
var wg sync.WaitGroup
const numWriters = 3
const numReaders = 5
const iterations = 100
var validReads atomic.Int64
// Writer goroutines
for w := range numWriters {
wg.Add(1)
go func(writerID int) {
defer wg.Done()
for i := range iterations {
cache.Reset([]int{writerID, i})
}
}(w)
}
// Reader goroutines
for range numReaders {
wg.Go(func() {
for range iterations {
data := cache.Get()
expired := cache.Expired()
// Verify data consistency: if we got data, it should be valid
if data != nil {
assert.Len(t, data, 2, "cached slice should always have exactly 2 elements")
assert.GreaterOrEqual(t, data[0], 0)
assert.Less(t, data[0], numWriters)
assert.GreaterOrEqual(t, data[1], 0)
assert.Less(t, data[1], iterations)
validReads.Add(1)
}
// Expired is a valid boolean - just verify it doesn't panic
_ = expired
}
})
}
wg.Wait()
// After all writes complete, cache should have valid final state
finalData := cache.Get()
assert.NotNil(t, finalData, "cache should have data after writes")
assert.Len(t, finalData, 2, "final data should have 2 elements")
assert.False(t, cache.Expired(), "cache should not be expired")
}