package server
import (
"fmt"
"reflect"
"testing"
"time"
"k8s.io/kubernetes/pkg/util/clock"
"k8s.io/kubernetes/pkg/util/diff"
)
const (
allowedDeviation = time.Millisecond * 10
ttl1m = time.Minute
ttl5m = time.Minute * 5
ttl8m = time.Minute * 8
)
func TestRepositoryBucketAdd(t *testing.T) {
now := time.Now()
clock := clock.NewFakeClock(now)
generated := make([]bucketEntry, bucketSize)
for i := 0; i < bucketSize; i++ {
generated[i] = bucketEntry{
repository: fmt.Sprintf("gen%d", i),
evictOn: now.Add(ttl5m),
}
}
for _, tc := range []struct {
name string
ttl time.Duration
repos []string
entries []bucketEntry
expectedEntries []bucketEntry
}{
{
name: "no existing entries",
ttl: ttl5m,
repos: []string{"a", "b"},
expectedEntries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "no entries to add",
ttl: ttl5m,
entries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "add few new entries",
ttl: ttl8m,
repos: []string{"bmw", "audi"},
entries: []bucketEntry{
{
repository: "skoda",
evictOn: now.Add(ttl5m),
},
{
repository: "ford",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "skoda",
evictOn: now.Add(ttl5m),
},
{
repository: "ford",
evictOn: now.Add(ttl5m),
},
{
repository: "bmw",
evictOn: now.Add(ttl8m),
},
{
repository: "audi",
evictOn: now.Add(ttl8m),
},
},
},
{
name: "add existing entry with single item",
ttl: ttl8m,
repos: []string{"apple"},
entries: []bucketEntry{
{repository: "apple", evictOn: now.Add(ttl5m)},
},
expectedEntries: []bucketEntry{
{repository: "apple", evictOn: now.Add(ttl8m)},
},
},
{
name: "add existing entry with higher ttl",
ttl: ttl8m,
repos: []string{"apple"},
entries: []bucketEntry{
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
{
repository: "apple",
evictOn: now.Add(ttl8m),
},
},
},
{
name: "add existing entry with lower ttl",
ttl: ttl5m,
repos: []string{"orange"},
entries: []bucketEntry{
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
},
},
{
name: "add new entry with eviction",
ttl: ttl5m,
repos: []string{"banana"},
entries: []bucketEntry{
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
{
repository: "apple",
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
{
repository: "banana",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "all stale",
ttl: ttl5m,
repos: []string{"banana"},
entries: []bucketEntry{
{
repository: "orange",
},
{
repository: "apple",
},
{
repository: "pear",
},
},
expectedEntries: []bucketEntry{
{
repository: "banana",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "add multiple entries with middle ttl",
ttl: ttl5m,
repos: []string{"apple", "banana", "peach", "orange"},
entries: []bucketEntry{
{
repository: "melon",
},
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
{
repository: "pear",
evictOn: now.Add(ttl1m),
},
{
repository: "plum",
},
},
expectedEntries: []bucketEntry{
{
repository: "pear",
evictOn: now.Add(ttl1m),
},
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
{
repository: "banana",
evictOn: now.Add(ttl5m),
},
{
repository: "peach",
evictOn: now.Add(ttl5m),
},
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
},
},
{
name: "over bucket size",
ttl: ttl1m,
repos: []string{"new1", generated[2].repository, "new2", generated[4].repository},
entries: generated,
expectedEntries: append(
append([]bucketEntry{generated[3]}, generated[5:bucketSize]...),
bucketEntry{repository: "new1", evictOn: now.Add(ttl1m)},
generated[2],
bucketEntry{repository: "new2", evictOn: now.Add(ttl1m)},
generated[4]),
},
} {
b := repositoryBucket{
clock: clock,
list: tc.entries,
}
b.Add(tc.ttl, tc.repos...)
if len(b.list) != len(tc.expectedEntries) {
t.Errorf("[%s] got unexpected number of entries in bucket: %d != %d", tc.name, len(b.list), len(tc.expectedEntries))
}
for i := 0; i < len(b.list); i++ {
if i >= len(tc.expectedEntries) {
t.Errorf("[%s] index=%d got unexpected entry: %#+v", tc.name, i, b.list[i])
continue
}
a, b := b.list[i], tc.expectedEntries[i]
if !bucketEntriesEqual(a, b) {
t.Errorf("[%s] index=%d got unexpected entry: %#+v != %#+v", tc.name, i, a, b)
}
}
for i := len(b.list); i < len(tc.expectedEntries); i++ {
if i >= len(tc.expectedEntries) {
t.Errorf("[%s] index=%d missing expected entry %#+v", tc.name, i, tc.expectedEntries[i])
}
}
}
}
func TestRepositoryBucketAddOversize(t *testing.T) {
clock := clock.NewFakeClock(time.Now())
b := repositoryBucket{
clock: clock,
}
i := 0
for ; i < bucketSize; i++ {
ttl := time.Duration(uint64(ttl5m) * uint64(i))
b.Add(ttl, fmt.Sprintf("%d", i))
}
if len(b.list) != bucketSize {
t.Fatalf("unexpected number of items: %d != %d", len(b.list), bucketSize)
}
// make first three stale
clock.Step(ttl5m * 3)
if !b.Has("3") {
t.Fatalf("bucket does not contain repository 3")
}
if len(b.list) != bucketSize-3 {
t.Fatalf("unexpected number of items: %d != %d", len(b.list), bucketSize-3)
}
// add few repos one by one
for ; i < bucketSize+5; i++ {
ttl := time.Duration(uint64(ttl5m) * uint64(i))
b.Add(ttl, fmt.Sprintf("%d", i))
}
if len(b.list) != bucketSize {
t.Fatalf("unexpected number of items: %d != %d", len(b.list), bucketSize)
}
// add few repos at once
newRepos := []string{}
for ; i < bucketSize+10; i++ {
newRepos = append(newRepos, fmt.Sprintf("%d", i))
}
b.Add(ttl5m, newRepos...)
if len(b.list) != bucketSize {
t.Fatalf("unexpected number of items: %d != %d", len(b.list), bucketSize)
}
for j := 0; j < bucketSize; j++ {
expected := fmt.Sprintf("%d", i-bucketSize+j)
if b.list[j].repository != expected {
t.Fatalf("unexpected repository on index %d: %s != %s", j, b.list[j].repository, expected)
}
}
}
func TestRepositoryBucketRemove(t *testing.T) {
now := time.Now()
clock := clock.NewFakeClock(now)
for _, tc := range []struct {
name string
repos []string
entries []bucketEntry
expectedEntries []bucketEntry
}{
{
name: "no existing entries",
repos: []string{"a", "b"},
},
{
name: "no matching entries",
repos: []string{"c", "d"},
entries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "no entries to remove",
entries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "remove one matching",
repos: []string{"bmw", "skoda"},
entries: []bucketEntry{
{
repository: "skoda",
evictOn: now.Add(ttl5m),
},
{
repository: "ford",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "ford",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "remove existing entry with single item",
repos: []string{"apple"},
entries: []bucketEntry{
{repository: "apple", evictOn: now.Add(ttl5m)},
},
expectedEntries: []bucketEntry{},
},
{
name: "remove, no eviction",
repos: []string{"pear"},
entries: []bucketEntry{
{
repository: "orange",
},
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "orange",
},
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
},
},
{
name: "remove multiple matching",
repos: []string{"orange", "apple"},
entries: []bucketEntry{
{
repository: "orange",
evictOn: now.Add(ttl8m),
},
{
repository: "apple",
evictOn: now.Add(ttl5m),
},
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
},
expectedEntries: []bucketEntry{
{
repository: "pear",
evictOn: now.Add(ttl5m),
},
},
},
} {
b := repositoryBucket{
clock: clock,
list: tc.entries,
}
b.Remove(tc.repos...)
if len(b.list) != len(tc.expectedEntries) {
t.Errorf("[%s] got unexpected number of entries in bucket: %d != %d", tc.name, len(b.list), len(tc.expectedEntries))
}
for i := 0; i < len(b.list); i++ {
if i >= len(tc.expectedEntries) {
t.Errorf("[%s] index=%d got unexpected entry: %#+v", tc.name, i, b.list[i])
continue
}
a, b := b.list[i], tc.expectedEntries[i]
if !bucketEntriesEqual(a, b) {
t.Errorf("[%s] index=%d got unexpected entry: %#+v != %#+v", tc.name, i, a, b)
}
}
for i := len(b.list); i < len(tc.expectedEntries); i++ {
if i >= len(tc.expectedEntries) {
t.Errorf("[%s] index=%d missing expected entry %#+v", tc.name, i, tc.expectedEntries[i])
}
}
}
}
func TestRepositoryBucketCopy(t *testing.T) {
now := time.Now()
clock := clock.NewFakeClock(now)
ttl5m := time.Minute * 5
for _, tc := range []struct {
name string
entries []bucketEntry
expectedRepos []string
}{
{
name: "no entry",
expectedRepos: []string{},
},
{
name: "one stale entry",
entries: []bucketEntry{
{
repository: "1",
},
},
expectedRepos: []string{},
},
{
name: "two entries",
entries: []bucketEntry{
{
repository: "a",
evictOn: now.Add(ttl5m),
},
{
repository: "b",
evictOn: now.Add(ttl5m),
},
},
expectedRepos: []string{"a", "b"},
},
} {
b := repositoryBucket{
clock: clock,
list: tc.entries,
}
result := b.Copy()
if !reflect.DeepEqual(result, tc.expectedRepos) {
t.Errorf("[%s] got unexpected repo list: %s", tc.name, diff.ObjectGoPrintDiff(result, tc.expectedRepos))
}
}
}
func bucketEntriesEqual(a, b bucketEntry) bool {
if a.repository != b.repository || a.evictOn != b.evictOn {
return false
}
return true
}