knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/configmap/store_test.go (about)

     1  /*
     2  Copyright 2018 The Knative Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package configmap
    18  
    19  import (
    20  	"errors"
    21  	"os"
    22  	"os/exec"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"github.com/google/go-cmp/cmp/cmpopts"
    28  	corev1 "k8s.io/api/core/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  
    31  	. "knative.dev/pkg/logging/testing"
    32  )
    33  
    34  const (
    35  	config1 = "config-name-1"
    36  	config2 = "config-name-2"
    37  )
    38  
    39  var constructor = func(c *corev1.ConfigMap) (interface{}, error) {
    40  	return c.Name, nil
    41  }
    42  
    43  func TestStoreBadConstructors(t *testing.T) {
    44  	tests := []struct {
    45  		name        string
    46  		constructor interface{}
    47  	}{{
    48  		name:        "not a function",
    49  		constructor: "i'm pretending to be a function",
    50  	}, {
    51  		name:        "no function arguments",
    52  		constructor: func() (bool, error) { return true, nil },
    53  	}, {
    54  		name:        "single argument is not a configmap",
    55  		constructor: func(bool) (bool, error) { return true, nil },
    56  	}, {
    57  		name:        "single return",
    58  		constructor: func(*corev1.ConfigMap) error { return nil },
    59  	}, {
    60  		name:        "wrong second return",
    61  		constructor: func(*corev1.ConfigMap) (bool, bool) { return true, true },
    62  	}}
    63  
    64  	for _, test := range tests {
    65  		t.Run(test.name, func(t *testing.T) {
    66  			defer func() {
    67  				if r := recover(); r == nil {
    68  					t.Error("expected NewUntypedStore to panic")
    69  				}
    70  			}()
    71  
    72  			NewUntypedStore("store", nil, Constructors{
    73  				"test": test.constructor,
    74  			})
    75  		})
    76  	}
    77  }
    78  
    79  func TestStoreWatchConfigs(t *testing.T) {
    80  	store := NewUntypedStore(
    81  		"name",
    82  		TestLogger(t),
    83  		Constructors{
    84  			config1: constructor,
    85  			config2: constructor,
    86  		},
    87  	)
    88  
    89  	watcher := &mockWatcher{}
    90  	store.WatchConfigs(watcher)
    91  
    92  	want := []string{
    93  		config1,
    94  		config2,
    95  	}
    96  
    97  	got := watcher.watches
    98  
    99  	if diff := cmp.Diff(want, got, sortStrings); diff != "" {
   100  		t.Errorf("Unexpected configmap watches (-want, +got):\n%s", diff)
   101  	}
   102  }
   103  
   104  func TestOnAfterStore(t *testing.T) {
   105  	var calledFor string
   106  	store := NewUntypedStore(
   107  		"name",
   108  		TestLogger(t),
   109  		Constructors{
   110  			config1: constructor,
   111  			config2: constructor,
   112  		},
   113  		func(name string, value interface{}) {
   114  			calledFor = name
   115  		},
   116  	)
   117  
   118  	store.OnConfigChanged(&corev1.ConfigMap{
   119  		ObjectMeta: metav1.ObjectMeta{
   120  			Name: config1,
   121  		},
   122  	})
   123  
   124  	if calledFor != config1 {
   125  		t.Fatalf("calledFor = %s, want %s", calledFor, config1)
   126  	}
   127  
   128  	store.OnConfigChanged(&corev1.ConfigMap{
   129  		ObjectMeta: metav1.ObjectMeta{
   130  			Name: config2,
   131  		},
   132  	})
   133  
   134  	if calledFor != config2 {
   135  		t.Fatalf("calledFor = %s, want %s", calledFor, config2)
   136  	}
   137  }
   138  
   139  func TestStoreConfigChange(t *testing.T) {
   140  	store := NewUntypedStore(
   141  		"name",
   142  		TestLogger(t),
   143  		Constructors{
   144  			config1: constructor,
   145  			config2: constructor,
   146  		},
   147  	)
   148  
   149  	store.OnConfigChanged(&corev1.ConfigMap{
   150  		ObjectMeta: metav1.ObjectMeta{
   151  			Name: config1,
   152  		},
   153  	})
   154  
   155  	result := store.UntypedLoad(config1)
   156  
   157  	if diff := cmp.Diff(result, config1); diff != "" {
   158  		t.Error("Expected loaded value diff:", diff)
   159  	}
   160  
   161  	result = store.UntypedLoad(config2)
   162  
   163  	if diff := cmp.Diff(result, nil); diff != "" {
   164  		t.Error("Unexpected loaded value diff:", diff)
   165  	}
   166  
   167  	store.OnConfigChanged(&corev1.ConfigMap{
   168  		ObjectMeta: metav1.ObjectMeta{
   169  			Name: config2,
   170  		},
   171  	})
   172  
   173  	result = store.UntypedLoad(config2)
   174  
   175  	if diff := cmp.Diff(result, config2); diff != "" {
   176  		t.Error("Expected loaded value diff:", diff)
   177  	}
   178  }
   179  
   180  func TestStoreFailedFirstConversionCrashes(t *testing.T) {
   181  	if os.Getenv("CRASH") == "1" {
   182  		constructor := func(c *corev1.ConfigMap) (interface{}, error) {
   183  			return nil, errors.New("failure")
   184  		}
   185  
   186  		store := NewUntypedStore("name", TestLogger(t),
   187  			Constructors{config1: constructor},
   188  		)
   189  
   190  		store.OnConfigChanged(&corev1.ConfigMap{
   191  			ObjectMeta: metav1.ObjectMeta{
   192  				Name: config1,
   193  			},
   194  		})
   195  		return
   196  	}
   197  
   198  	cmd := exec.Command(os.Args[0], "-test.run="+t.Name())
   199  	cmd.Env = append(os.Environ(), "CRASH=1")
   200  	err := cmd.Run()
   201  
   202  	var errExit *exec.ExitError
   203  	if errors.As(err, &errExit) && !errExit.Success() {
   204  		return
   205  	}
   206  	t.Fatal("process should have exited with status 1 - err", err)
   207  }
   208  
   209  func TestStoreFailedUpdate(t *testing.T) {
   210  	induceConstructorFailure := false
   211  
   212  	constructor := func(c *corev1.ConfigMap) (interface{}, error) {
   213  		if induceConstructorFailure {
   214  			return nil, errors.New("failure")
   215  		}
   216  
   217  		return time.Now().String(), nil
   218  	}
   219  
   220  	store := NewUntypedStore("name", TestLogger(t),
   221  		Constructors{config1: constructor},
   222  	)
   223  
   224  	store.OnConfigChanged(&corev1.ConfigMap{
   225  		ObjectMeta: metav1.ObjectMeta{
   226  			Name: config1,
   227  		},
   228  	})
   229  
   230  	firstLoad := store.UntypedLoad(config1)
   231  
   232  	induceConstructorFailure = true
   233  	store.OnConfigChanged(&corev1.ConfigMap{
   234  		ObjectMeta: metav1.ObjectMeta{
   235  			Name: config1,
   236  		},
   237  	})
   238  
   239  	secondLoad := store.UntypedLoad(config1)
   240  
   241  	if diff := cmp.Diff(firstLoad, secondLoad); diff != "" {
   242  		t.Error("Expected loaded value to remain the same dff:", diff)
   243  	}
   244  }
   245  
   246  type mockWatcher struct {
   247  	watches []string
   248  }
   249  
   250  func (w *mockWatcher) Watch(config string, o ...Observer) {
   251  	w.watches = append(w.watches, config)
   252  }
   253  
   254  func (*mockWatcher) Start(<-chan struct{}) error { return nil }
   255  
   256  var _ Watcher = (*mockWatcher)(nil)
   257  
   258  var sortStrings = cmpopts.SortSlices(func(x, y string) bool {
   259  	return x < y
   260  })