Add easy setup for simple controller

Also add tests; coverage up to 86.7%
This commit is contained in:
Daniel Smith
2015-04-07 16:00:37 -07:00
parent 065a8fa454
commit 880f922bb6
4 changed files with 433 additions and 7 deletions

View File

@@ -18,15 +18,18 @@ package framework_test
import (
"fmt"
"math/rand"
"sync"
"testing"
"time"
// "testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache"
"github.com/GoogleCloudPlatform/kubernetes/pkg/controller/framework"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/google/gofuzz"
)
func Example() {
@@ -34,7 +37,7 @@ func Example() {
source := framework.NewFakeControllerSource()
// This will hold the downstream state, as we know it.
downstream := cache.NewStore(cache.MetaNamespaceKeyFunc)
downstream := cache.NewStore(framework.DeletionHandlingMetaNamespaceKeyFunc)
// This will hold incoming changes. Note how we pass downstream in as a
// KeyLister, that way resync operations will result in the correct set
@@ -113,3 +116,272 @@ func Example() {
// b-controller
// c-framework
}
func ExampleInformer() {
// source simulates an apiserver object endpoint.
source := framework.NewFakeControllerSource()
// Let's do threadsafe output to get predictable test results.
outputSetLock := sync.Mutex{}
outputSet := util.StringSet{}
// Make a controller that immediately deletes anything added to it, and
// logs anything deleted.
_, controller := framework.NewInformer(
source,
&api.Pod{},
time.Millisecond*100,
framework.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
source.Delete(obj.(runtime.Object))
},
DeleteFunc: func(obj interface{}) {
key, err := framework.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
key = "oops something went wrong with the key"
}
// Record some output when items are deleted.
outputSetLock.Lock()
defer outputSetLock.Unlock()
outputSet.Insert(key)
},
},
)
// Run the controller and run it until we close stop.
stop := make(chan struct{})
controller.Run(stop)
// Let's add a few objects to the source.
for _, name := range []string{"a-hello", "b-controller", "c-framework"} {
// Note that these pods are not valid-- the fake source doesn't
// call validation or perform any other checking.
source.Add(&api.Pod{ObjectMeta: api.ObjectMeta{Name: name}})
}
// Let's wait for the controller to process the things we just added.
time.Sleep(500 * time.Millisecond)
close(stop)
outputSetLock.Lock()
for _, key := range outputSet.List() {
fmt.Println(key)
}
// Output:
// a-hello
// b-controller
// c-framework
}
func TestHammerController(t *testing.T) {
// This test executes a bunch of requests through the fake source and
// controller framework to make sure there's no locking/threading
// errors. If an error happens, it should hang forever or trigger the
// race detector.
// source simulates an apiserver object endpoint.
source := framework.NewFakeControllerSource()
// Let's do threadsafe output to get predictable test results.
outputSetLock := sync.Mutex{}
// map of key to operations done on the key
outputSet := map[string][]string{}
recordFunc := func(eventType string, obj interface{}) {
key, err := framework.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
t.Errorf("something wrong with key: %v", err)
key = "oops something went wrong with the key"
}
// Record some output when items are deleted.
outputSetLock.Lock()
defer outputSetLock.Unlock()
outputSet[key] = append(outputSet[key], eventType)
}
// Make a controller which just logs all the changes it gets.
_, controller := framework.NewInformer(
source,
&api.Pod{},
time.Millisecond*100,
framework.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { recordFunc("add", obj) },
UpdateFunc: func(oldObj, newObj interface{}) { recordFunc("update", newObj) },
DeleteFunc: func(obj interface{}) { recordFunc("delete", obj) },
},
)
// Run the controller and run it until we close stop.
stop := make(chan struct{})
go controller.Run(stop)
wg := sync.WaitGroup{}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Let's add a few objects to the source.
currentNames := util.StringSet{}
rs := rand.NewSource(rand.Int63())
f := fuzz.New().NilChance(.5).NumElements(0, 2).RandSource(rs)
r := rand.New(rs) // Mustn't use r and f concurrently!
for i := 0; i < 750; i++ {
var name string
var isNew bool
if currentNames.Len() == 0 || r.Intn(3) == 1 {
f.Fuzz(&name)
isNew = true
} else {
l := currentNames.List()
name = l[r.Intn(len(l))]
}
pod := &api.Pod{}
f.Fuzz(pod)
pod.ObjectMeta.Name = name
pod.ObjectMeta.Namespace = "default"
// Add, update, or delete randomly.
// Note that these pods are not valid-- the fake source doesn't
// call validation or perform any other checking.
if isNew {
currentNames.Insert(name)
source.Add(pod)
continue
}
switch r.Intn(2) {
case 0:
currentNames.Insert(name)
source.Modify(pod)
case 1:
currentNames.Delete(name)
source.Delete(pod)
}
}
}()
}
wg.Wait()
// Let's wait for the controller to finish processing the things we just added.
time.Sleep(100 * time.Millisecond)
close(stop)
outputSetLock.Lock()
t.Logf("got: %#v", outputSet)
}
func TestUpdate(t *testing.T) {
// This test is going to exercise the various paths that result in a
// call to update.
// source simulates an apiserver object endpoint.
source := framework.NewFakeControllerSource()
const (
FROM = "from"
ADD_MISSED = "missed the add event"
TO = "to"
)
// These are the transitions we expect to see; because this is
// asynchronous, there are a lot of valid possibilities.
type pair struct{ from, to string }
allowedTransitions := map[pair]bool{
pair{FROM, TO}: true,
pair{FROM, ADD_MISSED}: true,
pair{ADD_MISSED, TO}: true,
// Because a resync can happen when we've already observed one
// of the above but before the item is deleted.
pair{TO, TO}: true,
// Because a resync could happen before we observe an update.
pair{FROM, FROM}: true,
}
var testDoneWG sync.WaitGroup
// Make a controller that immediately deletes anything added to it, and
// logs anything deleted.
_, controller := framework.NewInformer(
source,
&api.Pod{},
time.Millisecond*1,
framework.ResourceEventHandlerFuncs{
UpdateFunc: func(oldObj, newObj interface{}) {
o, n := oldObj.(*api.Pod), newObj.(*api.Pod)
from, to := o.Labels["check"], n.Labels["check"]
if !allowedTransitions[pair{from, to}] {
t.Errorf("observed transition %q -> %q for %v", from, to, n.Name)
}
source.Delete(n)
},
DeleteFunc: func(obj interface{}) {
testDoneWG.Done()
},
},
)
// Run the controller and run it until we close stop.
stop := make(chan struct{})
go controller.Run(stop)
pod := func(name, check string) *api.Pod {
return &api.Pod{
ObjectMeta: api.ObjectMeta{
Name: name,
Labels: map[string]string{"check": check},
},
}
}
var wg sync.WaitGroup
tests := []func(string){
func(name string) {
defer wg.Done()
testDoneWG.Add(1)
name = "a-" + name
source.Add(pod(name, FROM))
source.Modify(pod(name, TO))
},
func(name string) {
defer wg.Done()
testDoneWG.Add(1)
name = "b-" + name
source.Add(pod(name, FROM))
source.ModifyDropWatch(pod(name, TO))
},
func(name string) {
defer wg.Done()
testDoneWG.Add(1)
name = "c-" + name
source.AddDropWatch(pod(name, FROM))
source.Modify(pod(name, ADD_MISSED))
source.Modify(pod(name, TO))
},
func(name string) {
defer wg.Done()
testDoneWG.Add(1)
name = "d-" + name
source.Add(pod(name, FROM))
},
}
// run every test a few times, in parallel
fuzzer := fuzz.New()
for i := 0; i < 20; i++ {
for _, f := range tests {
wg.Add(1)
var name string
for len(name) < 10 {
fuzzer.Fuzz(&name)
}
go f(name)
}
}
wg.Wait()
// Let's wait for the controller to process the things we just added.
testDoneWG.Wait()
close(stop)
}