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  }