1
0
mirror of https://github.com/rancher/steve.git synced 2025-07-01 09:12:12 +00:00
steve/pkg/sqlcache/informer/synthetic_watcher_test.go

191 lines
5.6 KiB
Go
Raw Normal View History

/*
Copyright 2024 SUSE LLC
*/
package informer
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
)
func TestSyntheticWatcher(t *testing.T) {
dynamicClient := NewMockResourceInterface(gomock.NewController(t))
var err error
cs1 := v1.ComponentStatus{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ComponentStatus",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cs1",
UID: "1",
ResourceVersion: "rv1.1",
},
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs1"}},
}
cs2 := v1.ComponentStatus{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ComponentStatus",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cs2",
UID: "2",
ResourceVersion: "rv2.1",
},
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs2"}},
}
cs3 := v1.ComponentStatus{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ComponentStatus",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cs3",
UID: "3",
ResourceVersion: "rv3.1",
},
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs3"}},
}
cs4 := v1.ComponentStatus{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ComponentStatus",
},
ObjectMeta: metav1.ObjectMeta{
Name: "cs4",
UID: "4",
ResourceVersion: "rv4.1",
},
Conditions: []v1.ComponentCondition{v1.ComponentCondition{Type: "Healthy", Status: v1.ConditionTrue, Message: "hi from cs4"}},
}
list, err := makeCSList(cs1, cs2, cs3, cs4)
assert.Nil(t, err)
dynamicClient.EXPECT().List(gomock.Any(), gomock.Any()).Return(list, nil)
// Make copies to avoid modifying objects before the watcher has processed them.
cs1b := cs1.DeepCopy()
cs1b.ObjectMeta.ResourceVersion = "rv1.2"
cs2b := cs2.DeepCopy()
cs2b.ObjectMeta.ResourceVersion = "rv2.2"
list2, err := makeCSList(*cs1b, *cs2b, cs4)
assert.Nil(t, err)
dynamicClient.EXPECT().List(gomock.Any(), gomock.Any()).AnyTimes().Return(list2, nil)
ctx, cancel := context.WithCancel(context.Background())
sw := newSyntheticWatcher(ctx, cancel)
pollingInterval := 10 * time.Millisecond
watchFunc := func(options metav1.ListOptions) (watch.Interface, error) {
return sw.watch(dynamicClient, options, pollingInterval)
}
options := metav1.ListOptions{}
w, err := watchFunc(options)
assert.Nil(t, err)
errChan := make(chan error)
results := make([]processedObjectInfo, 0)
var wg sync.WaitGroup
wg.Add(2)
go func() {
results, err = handleAnyWatch(w, errChan, sw.stopChan)
wg.Done()
}()
go func() {
time.Sleep(40 * time.Millisecond)
sw.stopChan <- struct{}{}
wg.Done()
}()
wg.Wait()
// Verify we get what we expected to see
assert.Len(t, results, 8)
for i, _ := range list.Items {
assert.Equal(t, "added-result", results[i].eventName)
}
assert.Equal(t, "modified-result", results[len(list.Items)].eventName)
assert.Equal(t, "modified-result", results[len(list.Items)+1].eventName)
assert.Equal(t, "deleted-result", results[len(list.Items)+2].eventName)
assert.Equal(t, "stop", results[7].eventName)
// We can't really assert that the events get the correct timestamps on them
// because they can be held up in the channel for unexpected durations. I did have
// assert.Greater(t, float64(timeDelta), 0.9*float64(pollingInterval))
// but saw a failure -- the interval was actually 0.75 * pollingInterval.
// So there's no point testing that.
assert.Greater(t, results[4].createdAt, results[0].createdAt)
}
func makeCSList(objs ...v1.ComponentStatus) (*unstructured.UnstructuredList, error) {
unList := make([]unstructured.Unstructured, len(objs))
for i, cs := range objs {
unst, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&cs)
if err != nil {
return nil, err
}
unList[i] = unstructured.Unstructured{Object: unst}
}
list := &unstructured.UnstructuredList{
Items: unList,
}
return list, nil
}
type processedObjectInfo struct {
createdAt time.Time
eventName string
payload interface{}
}
func makeProcessedObject(eventName string, payload interface{}) processedObjectInfo {
return processedObjectInfo{
createdAt: time.Now(),
eventName: eventName,
payload: payload,
}
}
func handleAnyWatch(w watch.Interface,
errCh chan error,
stopCh <-chan struct{},
) ([]processedObjectInfo, error) {
results := make([]processedObjectInfo, 0)
loop:
for {
select {
case <-stopCh:
results = append(results, makeProcessedObject("stop", nil))
return results, nil
case err := <-errCh:
results = append(results, makeProcessedObject("error", err))
return results, err
case event, ok := <-w.ResultChan():
if !ok {
results = append(results, makeProcessedObject("bad-result", nil))
break loop
}
switch event.Type {
case watch.Added:
results = append(results, makeProcessedObject("added-result", &event))
case watch.Modified:
results = append(results, makeProcessedObject("modified-result", &event))
case watch.Deleted:
results = append(results, makeProcessedObject("deleted-result", &event))
case watch.Bookmark:
results = append(results, makeProcessedObject("bookmark-result", &event))
default:
results = append(results, makeProcessedObject("unexpected-result", &event))
}
}
}
return results, nil
}