github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/gerrit/client/syncer_test.go (about) 1 /* 2 Copyright 2021 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 client 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "os" 25 "path/filepath" 26 "sync" 27 "testing" 28 "time" 29 30 "cloud.google.com/go/storage" 31 "github.com/google/go-cmp/cmp" 32 "sigs.k8s.io/prow/pkg/config" 33 "sigs.k8s.io/prow/pkg/io" 34 ) 35 36 type fakeOpener struct{} 37 38 func (o fakeOpener) Reader(ctx context.Context, path string) (io.ReadCloser, error) { 39 return nil, storage.ErrObjectNotExist 40 } 41 42 func (o fakeOpener) Writer(ctx context.Context, path string, _ ...io.WriterOptions) (io.WriteCloser, error) { 43 return nil, errors.New("do not call Writer") 44 } 45 46 func TestSyncTime(t *testing.T) { 47 48 dir := t.TempDir() 49 path := filepath.Join(dir, "value.txt") 50 var noCreds string 51 ctx := context.Background() 52 open, err := io.NewOpener(ctx, noCreds, noCreds) 53 if err != nil { 54 t.Fatalf("Failed to create opener: %v", err) 55 } 56 57 st := SyncTime{ 58 path: path, 59 opener: open, 60 ctx: ctx, 61 } 62 testProjects := map[string]map[string]*config.GerritQueryFilter{ 63 "foo": { 64 "bar": {}, 65 }, 66 } 67 now := time.Now() 68 if err := st.Init(testProjects); err != nil { 69 t.Fatalf("Failed init: %v", err) 70 } 71 cur := st.Current()["foo"]["bar"] 72 if now.After(cur) { 73 t.Fatalf("%v should be >= time before init was called: %v", cur, now) 74 } 75 76 earlier := now.Add(-time.Hour) 77 later := now.Add(time.Hour) 78 79 if err := st.Update(LastSyncState{"foo": {"bar": earlier}}); err != nil { 80 t.Fatalf("Failed update: %v", err) 81 } 82 if actual := st.Current()["foo"]["bar"]; !actual.Equal(cur) { 83 t.Errorf("Update(%v) should not have reduced value from %v, got %v", earlier, cur, actual) 84 } 85 86 if err := st.Update(LastSyncState{"foo": {"bar": later}}); err != nil { 87 t.Fatalf("Failed update: %v", err) 88 } 89 if actual := st.Current()["foo"]["bar"]; !actual.After(cur) { 90 t.Errorf("Update(%v) did not move current value to after %v, got %v", later, cur, actual) 91 } 92 93 expected := later 94 st = SyncTime{ 95 path: path, 96 opener: open, 97 ctx: ctx, 98 } 99 if err := st.Init(testProjects); err != nil { 100 t.Fatalf("Failed init: %v", err) 101 } 102 if actual := st.Current()["foo"]["bar"]; !actual.Equal(expected) { 103 t.Errorf("init() failed to reload %v, got %v", expected, actual) 104 } 105 // Make sure update can work 106 if err := st.update(map[string]map[string]*config.GerritQueryFilter{"foo-updated": {"bar-updated": nil}}); err != nil { 107 t.Fatalf("Failed update: %v", err) 108 } 109 { 110 gotState := st.Current() 111 if gotRepos, ok := gotState["foo-updated"]; !ok { 112 t.Fatal("Update() org failed.") 113 } else if _, ok := gotRepos["bar-updated"]; !ok { 114 t.Fatal("Update() repo failed.") 115 } 116 } 117 118 st = SyncTime{ 119 path: path, 120 opener: fakeOpener{}, // return storage.ErrObjectNotExist on open 121 ctx: ctx, 122 } 123 if err := st.Init(testProjects); err != nil { 124 t.Fatalf("Failed init: %v", err) 125 } 126 if actual := st.Current()["foo"]["bar"]; now.After(actual) || actual.After(later) { 127 t.Fatalf("should initialize to start %v <= actual <= later %v, but got %v", now, later, actual) 128 } 129 } 130 131 // TestSyncTimeThreadSafe ensures that the sync time can be updated threadsafe 132 // without lock. 133 func TestSyncTimeThreadSafe(t *testing.T) { 134 dir := t.TempDir() 135 path := filepath.Join(dir, "value.txt") 136 var noCreds string 137 ctx := context.Background() 138 open, err := io.NewOpener(ctx, noCreds, noCreds) 139 if err != nil { 140 t.Fatalf("Failed to create opener: %v", err) 141 } 142 143 st := SyncTime{ 144 path: path, 145 opener: open, 146 ctx: ctx, 147 } 148 testProjects := map[string]map[string]*config.GerritQueryFilter{ 149 "foo1": { 150 "bar1": {}, 151 }, 152 "foo2": { 153 "bar2": {}, 154 }, 155 } 156 if err := st.Init(testProjects); err != nil { 157 t.Fatalf("Failed init: %v", err) 158 } 159 160 // This is for detecting threading issue, running 100 times should be 161 // sufficient for catching the issue. 162 for i := 0; i < 100; i++ { 163 // Two threads, one update foo1, the other update foo2 164 var wg sync.WaitGroup 165 wg.Add(2) 166 later := time.Now().Add(time.Hour) 167 var threadErr error 168 go func() { 169 defer wg.Done() 170 syncTime := st.Current() 171 latest := syncTime.DeepCopy() 172 latest["foo1"]["bar1"] = later 173 if err := st.Update(latest); err != nil { 174 threadErr = fmt.Errorf("failed update: %v", err) 175 } 176 }() 177 178 go func() { 179 defer wg.Done() 180 syncTime := st.Current() 181 latest := syncTime.DeepCopy() 182 latest["foo2"]["bar2"] = later 183 if err := st.Update(latest); err != nil { 184 threadErr = fmt.Errorf("failed update: %v", err) 185 } 186 }() 187 188 wg.Wait() 189 if threadErr != nil { 190 t.Fatalf("Failed running goroutines: %v", err) 191 } 192 193 want := LastSyncState(map[string]map[string]time.Time{ 194 "foo1": {"bar1": later}, 195 "foo2": {"bar2": later}, 196 }) 197 198 if diff := cmp.Diff(st.Current(), want); diff != "" { 199 t.Fatalf("Mismatch. Want(-), got(+):\n%s", diff) 200 } 201 } 202 } 203 204 func TestNewProjectAddition(t *testing.T) { 205 dir := t.TempDir() 206 path := filepath.Join(dir, "value.txt") 207 208 testTime := time.Now().Add(-time.Minute) 209 testStVal := LastSyncState{"foo": {"bar": testTime}} 210 testStValBytes, _ := json.Marshal(testStVal) 211 _ = os.WriteFile(path, testStValBytes, os.ModePerm) 212 213 var noCreds string 214 ctx := context.Background() 215 open, err := io.NewOpener(ctx, noCreds, noCreds) 216 if err != nil { 217 t.Fatalf("Failed to create opener: %v", err) 218 } 219 220 testProjects := map[string]map[string]*config.GerritQueryFilter{ 221 "foo": { 222 "bar": {}, 223 }, 224 "qwe": { 225 "qux": {}, 226 }, 227 } 228 229 st := SyncTime{ 230 path: path, 231 opener: open, 232 ctx: ctx, 233 } 234 235 if err := st.Init(testProjects); err != nil { 236 t.Fatalf("Failed init: %v", err) 237 } 238 if _, ok := st.val["qwe"]; !ok { 239 t.Error("expected tracker to initialize a new entry for qwe, but did not") 240 } 241 if _, ok := st.val["qwe"]["qux"]; !ok { 242 t.Error("expected tracker to initialize a new entry for qwe/qux, but did not") 243 } 244 }