/* Copyright 2022 The Karmada 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 store import ( "context" "encoding/base64" "reflect" "sync" "testing" "time" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" ) func Test_watchMux_StopBySelf(t *testing.T) { m := newWatchMux() for i := 0; i < 10; i++ { w := newFakeWatcher(wait.NeverStop) m.AddSource(w, func(watch.Event) {}) go func() { ticker := time.NewTimer(time.Second) for { ticker.Reset(time.Millisecond) <-ticker.C if _, stopped := w.TryAdd(nil); stopped { return } } }() } m.Start() var recvCount int32 time.AfterFunc(time.Second, m.Stop) timeout := time.After(time.Second * 5) for { select { case _, ok := <-m.ResultChan(): if !ok { if recvCount == 0 { t.Error("receive no events") } return } recvCount++ case <-timeout: t.Error("timeout") m.Stop() return } } } func Test_watchMux_StopBySource(t *testing.T) { m := newWatchMux() defer m.Stop() ctx, cancel := context.WithTimeout(context.TODO(), time.Second) defer cancel() for i := 0; i < 10; i++ { w := newFakeWatcher(ctx.Done()) m.AddSource(w, func(watch.Event) {}) go func() { ticker := time.NewTimer(time.Second) for { ticker.Reset(time.Millisecond) <-ticker.C if _, stopped := w.TryAdd(nil); stopped { return } } }() } m.Start() var recvCount int32 timeout := time.After(time.Second * 5) for { select { case _, ok := <-m.ResultChan(): if !ok { if recvCount == 0 { t.Error("receive no events") } return } recvCount++ case <-timeout: t.Error("timeout") return } } } type fakeWatcher struct { result chan watch.Event done chan struct{} stopped bool sync.Mutex } func newFakeWatcher(stopCh <-chan struct{}) *fakeWatcher { f := &fakeWatcher{ result: make(chan watch.Event), done: make(chan struct{}), } go func() { defer f.Stop() select { case <-stopCh: return case <-f.done: return } }() return f } func (f *fakeWatcher) Stop() { f.Lock() defer f.Unlock() if !f.stopped { close(f.result) close(f.done) f.stopped = true } } func (f *fakeWatcher) ResultChan() <-chan watch.Event { return f.result } func (f *fakeWatcher) TryAdd(obj runtime.Object) (added bool, stopped bool) { f.Lock() defer f.Unlock() if f.stopped { return false, true } select { case f.result <- watch.Event{Type: watch.Added, Object: obj}: return true, false default: return false, false } } func Test_newMultiClusterResourceVersionFromString(t *testing.T) { type args struct { s string } tests := []struct { name string args args want *multiClusterResourceVersion }{ { name: "empty", args: args{ s: "", }, want: &multiClusterResourceVersion{ rvs: map[string]string{}, }, }, { name: "zero", args: args{ s: "0", }, want: &multiClusterResourceVersion{ rvs: map[string]string{}, isZero: true, }, }, { name: "decode error", args: args{ s: "`not encoded`", }, want: &multiClusterResourceVersion{ rvs: map[string]string{}, }, }, { name: "not a json", args: args{ s: base64.RawURLEncoding.EncodeToString([]byte(`not a json`)), }, want: &multiClusterResourceVersion{ rvs: map[string]string{}, }, }, { name: "success - normal", args: args{ s: base64.RawURLEncoding.EncodeToString([]byte(`{"cluster1":"1","cluster2":"2"}`)), }, want: &multiClusterResourceVersion{ rvs: map[string]string{ "cluster1": "1", "cluster2": "2", }, }, }, { name: "success - empty cluster name", args: args{ s: base64.RawURLEncoding.EncodeToString([]byte(`{"":"1","cluster2":"2"}`)), }, want: &multiClusterResourceVersion{ rvs: map[string]string{ "": "1", "cluster2": "2", }, }, }, { name: "success - empty ResourceVersion", args: args{ s: base64.RawURLEncoding.EncodeToString([]byte(`{"cluster1":"","cluster2":""}`)), }, want: &multiClusterResourceVersion{ rvs: map[string]string{ "cluster1": "", "cluster2": "", }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := newMultiClusterResourceVersionFromString(tt.args.s) if !reflect.DeepEqual(got, tt.want) { t.Errorf("newMultiClusterResourceVersionFromString() = %#v, want %#v", got, tt.want) } }) } } func Test_multiClusterResourceVersion_get(t *testing.T) { type fields struct { rvs map[string]string isZero bool } type args struct { cluster string } tests := []struct { name string fields fields args args want string }{ { name: "zero", fields: fields{ isZero: true, rvs: map[string]string{}, }, args: args{ cluster: "cluster1", }, want: "0", }, { name: "not exist", fields: fields{ rvs: map[string]string{}, }, args: args{ cluster: "cluster1", }, want: "", }, { name: "get success", fields: fields{ rvs: map[string]string{ "cluster1": "1", }, }, args: args{ cluster: "cluster1", }, want: "1", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &multiClusterResourceVersion{ rvs: tt.fields.rvs, isZero: tt.fields.isZero, } if got := m.get(tt.args.cluster); got != tt.want { t.Errorf("get() = %v, want %v", got, tt.want) } }) } } func Test_multiClusterResourceVersion_String(t *testing.T) { type fields struct { rvs map[string]string isZero bool } tests := []struct { name string fields fields want string }{ { name: "zero", fields: fields{ isZero: true, rvs: map[string]string{}, }, want: "0", }, { name: "empty", fields: fields{ rvs: map[string]string{}, }, want: "", }, { name: "get success - normal", fields: fields{ rvs: map[string]string{ "cluster1": "1", "cluster2": "2", }, }, want: base64.RawURLEncoding.EncodeToString([]byte(`{"cluster1":"1","cluster2":"2"}`)), }, { name: "get success - empty cluster name", fields: fields{ rvs: map[string]string{ "": "1", "cluster2": "2", }, }, want: base64.RawURLEncoding.EncodeToString([]byte(`{"":"1","cluster2":"2"}`)), }, { name: "get success - empty ResourceVersion", fields: fields{ rvs: map[string]string{ "cluster1": "", "cluster2": "", }, }, want: base64.RawURLEncoding.EncodeToString([]byte(`{"cluster1":"","cluster2":""}`)), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := &multiClusterResourceVersion{ rvs: tt.fields.rvs, isZero: tt.fields.isZero, } if got := m.String(); got != tt.want { t.Errorf("String() = %v, want %v", got, tt.want) } }) } } func Test_newMultiClusterContinueFromString(t *testing.T) { type args struct { s string } tests := []struct { name string args args want multiClusterContinue }{ { name: "empty", args: args{ s: "", }, want: multiClusterContinue{}, }, { name: "not encoded", args: args{ s: "not encoded", }, want: multiClusterContinue{}, }, { name: "not json", args: args{ s: base64.RawURLEncoding.EncodeToString([]byte("not json")), }, want: multiClusterContinue{}, }, { name: "success", args: args{ s: base64.RawURLEncoding.EncodeToString([]byte(`{"cluster":"cluster1","continue":"1"}`)), }, want: multiClusterContinue{ Cluster: "cluster1", Continue: "1", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := newMultiClusterContinueFromString(tt.args.s); !reflect.DeepEqual(got, tt.want) { t.Errorf("newMultiClusterContinueFromString() = %v, want %v", got, tt.want) } }) } } func Test_multiClusterContinue_String(t *testing.T) { type fields struct { RV string Cluster string Continue string } tests := []struct { name string fields fields want string }{ { name: "empty", fields: fields{ RV: "", Cluster: "", Continue: "", }, want: "", }, { name: "success", fields: fields{ RV: "123", Cluster: "cluster1", Continue: "1", }, want: base64.RawURLEncoding.EncodeToString([]byte(`{"rv":"123","cluster":"cluster1","continue":"1"}`)), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &multiClusterContinue{ RV: tt.fields.RV, Cluster: tt.fields.Cluster, Continue: tt.fields.Continue, } if got := c.String(); got != tt.want { t.Errorf("String() = %v, want %v", got, tt.want) } }) } } func TestRemoveCacheSourceAnnotation(t *testing.T) { type args struct { obj runtime.Object } type want struct { obj runtime.Object changed bool } tests := []struct { name string args args want want }{ { name: "not a meta", args: args{ obj: &metav1.Status{}, }, want: want{ changed: false, obj: &metav1.Status{}, }, }, { name: "annotation not exist", args: args{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{}, }, }, want: want{ changed: false, obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{}, }, }, }, { name: "remove annotation", args: args{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{ clusterv1alpha1.CacheSourceAnnotationKey: "cluster1", }, }, }, }, want: want{ changed: true, obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Annotations: map[string]string{}, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := RemoveCacheSourceAnnotation(tt.args.obj) if got != tt.want.changed { t.Errorf("RemoveCacheSourceAnnotation() = %v, want %v", got, tt.want) } if !reflect.DeepEqual(tt.args.obj, tt.want.obj) { t.Errorf("RemoveCacheSourceAnnotation() got obj = %#v, want %#v", tt.args.obj, tt.want.obj) } }) } } func TestRecoverClusterResourceVersion(t *testing.T) { type args struct { obj runtime.Object cluster string } type want struct { obj runtime.Object changed bool } tests := []struct { name string args args want want }{ { name: "not a meta", args: args{ obj: &metav1.Status{}, cluster: "cluster1", }, want: want{ changed: false, obj: &metav1.Status{}, }, }, { name: "rv is empty", args: args{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: "", }, }, cluster: "cluster1", }, want: want{ changed: false, obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{}, }, }, }, { name: "rv is 0", args: args{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: "0", }, }, cluster: "cluster1", }, want: want{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: "0", }, }, changed: false, }, }, { name: "single cluster rv", args: args{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: "1000", }, }, cluster: "cluster1", }, want: want{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: "1000", }, }, changed: false, }, }, { name: "cluster rv not set", args: args{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: base64.RawURLEncoding.EncodeToString([]byte(`{}`)), }, }, cluster: "cluster1", }, want: want{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{}, }, changed: true, }, }, { name: "recover cluster rv", args: args{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: base64.RawURLEncoding.EncodeToString([]byte(`{"cluster1":"1","cluster2":"2"}`)), }, }, cluster: "cluster1", }, want: want{ obj: &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ ResourceVersion: "1", }, }, changed: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := RecoverClusterResourceVersion(tt.args.obj, tt.args.cluster) if got != tt.want.changed { t.Errorf("RecoverClusterResourceVersion() changed = %v, want %v", got, tt.want.changed) } if !reflect.DeepEqual(tt.args.obj, tt.want.obj) { t.Errorf("RecoverClusterResourceVersion() got obj = %#v, want %#v", tt.args.obj, tt.want.obj) } }) } } func TestBuildMultiClusterResourceVersion(t *testing.T) { type args struct { clusterResourceMap map[string]string } tests := []struct { name string args args want string }{ { name: "empty", args: args{ clusterResourceMap: nil, }, want: "", }, { name: "success", args: args{ clusterResourceMap: map[string]string{ "cluster1": "1", }, }, want: base64.RawURLEncoding.EncodeToString([]byte(`{"cluster1":"1"}`)), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := BuildMultiClusterResourceVersion(tt.args.clusterResourceMap); got != tt.want { t.Errorf("BuildMultiClusterResourceVersion() = %v, want %v", got, tt.want) } }) } } func TestMultiNamespace_Add(t *testing.T) { type fields struct { allNamespaces bool namespaces sets.Set[string] } type args struct { ns string } tests := []struct { name string fields fields args args want *MultiNamespace }{ { name: "add foo ns to empty selector", fields: fields{ allNamespaces: false, namespaces: sets.New[string](), }, args: args{ ns: "foo", }, want: &MultiNamespace{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, }, { name: "add all namespaces to empty selector", fields: fields{ allNamespaces: false, namespaces: sets.New[string](), }, args: args{ ns: metav1.NamespaceAll, }, want: &MultiNamespace{ allNamespaces: true, namespaces: nil, }, }, { name: "add new namespace to non empty selector", fields: fields{ allNamespaces: false, namespaces: sets.New[string]("bar"), }, args: args{ ns: "foo", }, want: &MultiNamespace{ allNamespaces: false, namespaces: sets.New[string]("foo", "bar"), }, }, { name: "add existed namespace to non empty selector", fields: fields{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, args: args{ ns: "foo", }, want: &MultiNamespace{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, }, { name: "add all namespaces to non empty selector", fields: fields{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, args: args{ ns: metav1.NamespaceAll, }, want: &MultiNamespace{ allNamespaces: true, namespaces: nil, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { n := &MultiNamespace{ allNamespaces: tt.fields.allNamespaces, namespaces: tt.fields.namespaces, } n.Add(tt.args.ns) if got := n; !got.Equal(tt.want) { t.Errorf("Add()=%v, want %v", got, tt.want) } }) } } func TestMultiNamespace_Contains(t *testing.T) { type fields struct { allNamespaces bool namespaces sets.Set[string] } type args struct { ns string } tests := []struct { name string fields fields args args want bool }{ { name: "All namespaces contains any ns", fields: fields{ allNamespaces: true, }, args: args{ns: "foo"}, want: true, }, { name: "some namespaces contains target ns", fields: fields{ namespaces: sets.New[string]("foo", "bar"), }, args: args{ns: "foo"}, want: true, }, { name: "some namespaces not contains target ns", fields: fields{ namespaces: sets.New[string]("foo", "bar"), }, args: args{ns: "baz"}, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { n := &MultiNamespace{ allNamespaces: tt.fields.allNamespaces, namespaces: tt.fields.namespaces, } if got := n.Contains(tt.args.ns); got != tt.want { t.Errorf("Contains() = %v, want %v", got, tt.want) } }) } } func TestMultiNamespace_Single(t *testing.T) { type fields struct { allNamespaces bool namespaces sets.Set[string] } tests := []struct { name string fields fields want string want1 bool }{ { name: "all namespaces always return false", fields: fields{ allNamespaces: true, }, want: "", want1: false, }, { name: "contains only one ns", fields: fields{ namespaces: sets.New[string]("foo"), }, want: "foo", want1: true, }, { name: "contains only two ns", fields: fields{ namespaces: sets.New[string]("foo", "bar"), }, want: "", want1: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { n := &MultiNamespace{ allNamespaces: tt.fields.allNamespaces, namespaces: tt.fields.namespaces, } got, got1 := n.Single() if got != tt.want { t.Errorf("Single() got = %v, want %v", got, tt.want) } if got1 != tt.want1 { t.Errorf("Single() got1 = %v, want %v", got1, tt.want1) } }) } } func TestMultiNamespace_Equal(t *testing.T) { type fields struct { allNamespaces bool namespaces sets.Set[string] } type args struct { another *MultiNamespace } tests := []struct { name string fields fields args args want bool }{ { name: "empty vs empty", fields: fields{ allNamespaces: false, namespaces: sets.New[string](), }, args: args{ another: NewMultiNamespace(), }, want: true, }, { name: "empty vs non empty", fields: fields{ allNamespaces: false, namespaces: sets.New[string](), }, args: args{ another: &MultiNamespace{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, }, want: false, }, { name: "empty vs all namespaces", fields: fields{ allNamespaces: false, namespaces: sets.New[string](), }, args: args{ another: &MultiNamespace{ allNamespaces: true, namespaces: nil, }, }, want: false, }, { name: "non empty vs empty", fields: fields{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, args: args{ another: NewMultiNamespace(), }, want: false, }, { name: "non empty vs non empty, but not same", fields: fields{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, args: args{ another: &MultiNamespace{ allNamespaces: false, namespaces: sets.New[string]("bar"), }, }, want: false, }, { name: "non empty vs same", fields: fields{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, args: args{ another: &MultiNamespace{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, }, want: true, }, { name: "non empty vs all namespaces", fields: fields{ allNamespaces: false, namespaces: sets.New[string]("foo"), }, args: args{ another: &MultiNamespace{ allNamespaces: true, }, }, want: false, }, { name: "all namespaces vs empty", fields: fields{ allNamespaces: true, }, args: args{ another: NewMultiNamespace(), }, want: false, }, { name: "all namespaces vs non empty", fields: fields{ allNamespaces: true, }, args: args{ another: &MultiNamespace{ namespaces: sets.New[string]("foo"), }, }, want: false, }, { name: "all namespaces vs all namespaces", fields: fields{ allNamespaces: true, }, args: args{ another: &MultiNamespace{ allNamespaces: true, }, }, want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { n := &MultiNamespace{ allNamespaces: tt.fields.allNamespaces, namespaces: tt.fields.namespaces, } if got := n.Equal(tt.args.another); got != tt.want { t.Errorf("Equal() = %v, want %v", got, tt.want) } }) } }