...

Source file src/k8s.io/client-go/tools/cache/synctrack/synctrack_test.go

Documentation: k8s.io/client-go/tools/cache/synctrack

     1  /*
     2  Copyright 2022 The Kubernetes 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 synctrack
    18  
    19  import (
    20  	"strings"
    21  	"sync"
    22  	"time"
    23  
    24  	"testing"
    25  )
    26  
    27  func testSingleFileFuncs(upstreamHasSynced func() bool) (start func(), finished func(), hasSynced func() bool) {
    28  	tracker := SingleFileTracker{
    29  		UpstreamHasSynced: upstreamHasSynced,
    30  	}
    31  	return tracker.Start, tracker.Finished, tracker.HasSynced
    32  }
    33  
    34  func testAsyncFuncs(upstreamHasSynced func() bool) (start func(), finished func(), hasSynced func() bool) {
    35  	tracker := AsyncTracker[string]{
    36  		UpstreamHasSynced: upstreamHasSynced,
    37  	}
    38  	return func() { tracker.Start("key") }, func() { tracker.Finished("key") }, tracker.HasSynced
    39  }
    40  
    41  func TestBasicLogic(t *testing.T) {
    42  	table := []struct {
    43  		name      string
    44  		construct func(func() bool) (func(), func(), func() bool)
    45  	}{
    46  		{"SingleFile", testSingleFileFuncs},
    47  		{"Async", testAsyncFuncs},
    48  	}
    49  
    50  	for _, entry := range table {
    51  		t.Run(entry.name, func(t *testing.T) {
    52  			table := []struct {
    53  				synced       bool
    54  				start        bool
    55  				finish       bool
    56  				expectSynced bool
    57  			}{
    58  				{false, true, true, false},
    59  				{true, true, false, false},
    60  				{false, true, false, false},
    61  				{true, true, true, true},
    62  			}
    63  			for _, tt := range table {
    64  				Start, Finished, HasSynced := entry.construct(func() bool { return tt.synced })
    65  				if tt.start {
    66  					Start()
    67  				}
    68  				if tt.finish {
    69  					Finished()
    70  				}
    71  				got := HasSynced()
    72  				if e, a := tt.expectSynced, got; e != a {
    73  					t.Errorf("for %#v got %v (wanted %v)", tt, a, e)
    74  				}
    75  			}
    76  		})
    77  	}
    78  }
    79  
    80  func TestAsyncLocking(t *testing.T) {
    81  	aft := AsyncTracker[int]{UpstreamHasSynced: func() bool { return true }}
    82  
    83  	var wg sync.WaitGroup
    84  	for _, i := range []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10} {
    85  		wg.Add(1)
    86  		go func(i int) {
    87  			aft.Start(i)
    88  			go func() {
    89  				aft.Finished(i)
    90  				wg.Done()
    91  			}()
    92  		}(i)
    93  	}
    94  	wg.Wait()
    95  	if !aft.HasSynced() {
    96  		t.Errorf("async tracker must have made a threading error?")
    97  	}
    98  
    99  }
   100  
   101  func TestSingleFileCounting(t *testing.T) {
   102  	sft := SingleFileTracker{UpstreamHasSynced: func() bool { return true }}
   103  
   104  	for i := 0; i < 100; i++ {
   105  		sft.Start()
   106  	}
   107  	if sft.HasSynced() {
   108  		t.Fatal("Unexpectedly synced?")
   109  	}
   110  	for i := 0; i < 99; i++ {
   111  		sft.Finished()
   112  	}
   113  	if sft.HasSynced() {
   114  		t.Fatal("Unexpectedly synced?")
   115  	}
   116  
   117  	sft.Finished()
   118  	if !sft.HasSynced() {
   119  		t.Fatal("Unexpectedly not synced?")
   120  	}
   121  
   122  	// Calling an extra time will panic.
   123  	func() {
   124  		defer func() {
   125  			x := recover()
   126  			if x == nil {
   127  				t.Error("no panic?")
   128  				return
   129  			}
   130  			msg, ok := x.(string)
   131  			if !ok {
   132  				t.Errorf("unexpected panic value: %v", x)
   133  				return
   134  			}
   135  			if !strings.Contains(msg, "negative counter") {
   136  				t.Errorf("unexpected panic message: %v", msg)
   137  				return
   138  			}
   139  		}()
   140  		sft.Finished()
   141  	}()
   142  
   143  	// Negative counter still means it is synced
   144  	if !sft.HasSynced() {
   145  		t.Fatal("Unexpectedly not synced?")
   146  	}
   147  }
   148  
   149  func TestSingleFile(t *testing.T) {
   150  	table := []struct {
   151  		synced       bool
   152  		starts       int
   153  		stops        int
   154  		expectSynced bool
   155  	}{
   156  		{false, 1, 1, false},
   157  		{true, 1, 0, false},
   158  		{false, 1, 0, false},
   159  		{true, 1, 1, true},
   160  	}
   161  	for _, tt := range table {
   162  		sft := SingleFileTracker{UpstreamHasSynced: func() bool { return tt.synced }}
   163  		for i := 0; i < tt.starts; i++ {
   164  			sft.Start()
   165  		}
   166  		for i := 0; i < tt.stops; i++ {
   167  			sft.Finished()
   168  		}
   169  		got := sft.HasSynced()
   170  		if e, a := tt.expectSynced, got; e != a {
   171  			t.Errorf("for %#v got %v (wanted %v)", tt, a, e)
   172  		}
   173  	}
   174  
   175  }
   176  
   177  func TestNoStaleValue(t *testing.T) {
   178  	table := []struct {
   179  		name      string
   180  		construct func(func() bool) (func(), func(), func() bool)
   181  	}{
   182  		{"SingleFile", testSingleFileFuncs},
   183  		{"Async", testAsyncFuncs},
   184  	}
   185  
   186  	for _, entry := range table {
   187  		t.Run(entry.name, func(t *testing.T) {
   188  			var lock sync.Mutex
   189  			upstreamHasSynced := func() bool {
   190  				lock.Lock()
   191  				defer lock.Unlock()
   192  				return true
   193  			}
   194  
   195  			Start, Finished, HasSynced := entry.construct(upstreamHasSynced)
   196  
   197  			// Ordinarily the corresponding lock would be held and you wouldn't be
   198  			// able to call this function at this point.
   199  			if !HasSynced() {
   200  				t.Fatal("Unexpectedly not synced??")
   201  			}
   202  
   203  			Start()
   204  			if HasSynced() {
   205  				t.Fatal("Unexpectedly synced??")
   206  			}
   207  			Finished()
   208  			if !HasSynced() {
   209  				t.Fatal("Unexpectedly not synced??")
   210  			}
   211  
   212  			// Now we will prove that if the lock is held, you can't get a false
   213  			// HasSynced return.
   214  			lock.Lock()
   215  
   216  			// This goroutine calls HasSynced
   217  			var wg sync.WaitGroup
   218  			wg.Add(1)
   219  			go func() {
   220  				defer wg.Done()
   221  				if HasSynced() {
   222  					t.Error("Unexpectedly synced??")
   223  				}
   224  			}()
   225  
   226  			// This goroutine increments + unlocks. The sleep is to bias the
   227  			// runtime such that the other goroutine usually wins (it needs to work
   228  			// in both orderings, this one is more likely to be buggy).
   229  			go func() {
   230  				time.Sleep(time.Millisecond)
   231  				Start()
   232  				lock.Unlock()
   233  			}()
   234  
   235  			wg.Wait()
   236  		})
   237  	}
   238  
   239  }
   240  

View as plain text