github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/horologium/main_test.go (about)

     1  /*
     2  Copyright 2017 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 main
    18  
    19  import (
    20  	"context"
    21  	"flag"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"sigs.k8s.io/controller-runtime/pkg/client"
    29  	ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client"
    30  	fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
    31  
    32  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    33  	"sigs.k8s.io/prow/pkg/config"
    34  	"sigs.k8s.io/prow/pkg/flagutil"
    35  	configflagutil "sigs.k8s.io/prow/pkg/flagutil/config"
    36  )
    37  
    38  type fakeCron struct {
    39  	jobs []string
    40  }
    41  
    42  func (fc *fakeCron) SyncConfig(cfg *config.Config) error {
    43  	for _, p := range cfg.Periodics {
    44  		if p.Cron != "" {
    45  			fc.jobs = append(fc.jobs, p.Name)
    46  		}
    47  	}
    48  
    49  	return nil
    50  }
    51  
    52  func (fc *fakeCron) QueuedJobs() []string {
    53  	res := fc.jobs
    54  	fc.jobs = nil
    55  	return res
    56  }
    57  
    58  // Assumes there is one periodic job called "p" with an interval of one minute.
    59  func TestSync(t *testing.T) {
    60  	testcases := []struct {
    61  		testName string
    62  
    63  		jobName         string
    64  		jobComplete     bool
    65  		jobStartTimeAgo time.Duration
    66  
    67  		shouldStart bool
    68  	}{
    69  		{
    70  			testName:    "no job",
    71  			shouldStart: true,
    72  		},
    73  		{
    74  			testName:        "job with other name",
    75  			jobName:         "not-j",
    76  			jobComplete:     true,
    77  			jobStartTimeAgo: time.Hour,
    78  			shouldStart:     true,
    79  		},
    80  		{
    81  			testName:        "old, complete job",
    82  			jobName:         "j",
    83  			jobComplete:     true,
    84  			jobStartTimeAgo: time.Hour,
    85  			shouldStart:     true,
    86  		},
    87  		{
    88  			testName:        "old, incomplete job",
    89  			jobName:         "j",
    90  			jobComplete:     false,
    91  			jobStartTimeAgo: time.Hour,
    92  			shouldStart:     false,
    93  		},
    94  		{
    95  			testName:        "new, complete job",
    96  			jobName:         "j",
    97  			jobComplete:     true,
    98  			jobStartTimeAgo: time.Second,
    99  			shouldStart:     false,
   100  		},
   101  		{
   102  			testName:        "new, incomplete job",
   103  			jobName:         "j",
   104  			jobComplete:     false,
   105  			jobStartTimeAgo: time.Second,
   106  			shouldStart:     false,
   107  		},
   108  	}
   109  	for _, tc := range testcases {
   110  		cfg := config.Config{
   111  			ProwConfig: config.ProwConfig{
   112  				ProwJobNamespace: "prowjobs",
   113  			},
   114  			JobConfig: config.JobConfig{
   115  				Periodics: []config.Periodic{{JobBase: config.JobBase{Name: "j"}}},
   116  			},
   117  		}
   118  		cfg.Periodics[0].SetInterval(time.Minute)
   119  
   120  		var jobs []client.Object
   121  		now := time.Now()
   122  		if tc.jobName != "" {
   123  			job := &prowapi.ProwJob{
   124  				ObjectMeta: metav1.ObjectMeta{
   125  					Name:      "with-interval",
   126  					Namespace: "prowjobs",
   127  				},
   128  				Spec: prowapi.ProwJobSpec{
   129  					Type: prowapi.PeriodicJob,
   130  					Job:  tc.jobName,
   131  				},
   132  				Status: prowapi.ProwJobStatus{
   133  					StartTime: metav1.NewTime(now.Add(-tc.jobStartTimeAgo)),
   134  				},
   135  			}
   136  			complete := metav1.NewTime(now.Add(-time.Millisecond))
   137  			if tc.jobComplete {
   138  				job.Status.CompletionTime = &complete
   139  			}
   140  			jobs = append(jobs, job)
   141  		}
   142  		fakeProwJobClient := newCreateTrackingClient(jobs)
   143  		fc := &fakeCron{}
   144  		if err := sync(fakeProwJobClient, &cfg, fc, now); err != nil {
   145  			t.Fatalf("For case %s, didn't expect error: %v", tc.testName, err)
   146  		}
   147  
   148  		sawCreation := fakeProwJobClient.sawCreate
   149  		if tc.shouldStart != sawCreation {
   150  			t.Errorf("For case %s, did the wrong thing.", tc.testName)
   151  		}
   152  	}
   153  }
   154  
   155  // Assumes there is one periodic job called "p" with a minimum_interval of one minute.
   156  func TestSyncMinimumInterval(t *testing.T) {
   157  	testcases := []struct {
   158  		testName string
   159  
   160  		jobName         string
   161  		jobComplete     bool
   162  		jobStartTimeAgo time.Duration
   163  		// defaults to 1 ms
   164  		jobCompleteTimeAgo time.Duration
   165  
   166  		shouldStart bool
   167  	}{
   168  		{
   169  			testName:    "no job",
   170  			shouldStart: true,
   171  		},
   172  		{
   173  			testName:        "job with other name",
   174  			jobName:         "not-j",
   175  			jobComplete:     true,
   176  			jobStartTimeAgo: time.Hour,
   177  			shouldStart:     true,
   178  		},
   179  		{
   180  			testName:           "old, complete job",
   181  			jobName:            "j",
   182  			jobComplete:        true,
   183  			jobStartTimeAgo:    time.Hour,
   184  			jobCompleteTimeAgo: 30 * time.Minute,
   185  			shouldStart:        true,
   186  		},
   187  		{
   188  			testName:        "old, recently complete job",
   189  			jobName:         "j",
   190  			jobComplete:     true,
   191  			jobStartTimeAgo: time.Hour,
   192  			shouldStart:     false,
   193  		},
   194  		{
   195  			testName:        "old, incomplete job",
   196  			jobName:         "j",
   197  			jobComplete:     false,
   198  			jobStartTimeAgo: time.Hour,
   199  			shouldStart:     false,
   200  		},
   201  		{
   202  			testName:        "new, complete job",
   203  			jobName:         "j",
   204  			jobComplete:     true,
   205  			jobStartTimeAgo: time.Second,
   206  			shouldStart:     false,
   207  		},
   208  		{
   209  			testName:        "new, incomplete job",
   210  			jobName:         "j",
   211  			jobComplete:     false,
   212  			jobStartTimeAgo: time.Second,
   213  			shouldStart:     false,
   214  		},
   215  	}
   216  	for _, tc := range testcases {
   217  		cfg := config.Config{
   218  			ProwConfig: config.ProwConfig{
   219  				ProwJobNamespace: "prowjobs",
   220  			},
   221  			JobConfig: config.JobConfig{
   222  				Periodics: []config.Periodic{{JobBase: config.JobBase{Name: "j"}}},
   223  			},
   224  		}
   225  		cfg.Periodics[0].MinimumInterval = "1m"
   226  		cfg.Periodics[0].SetMinimumInterval(time.Minute)
   227  
   228  		var jobs []client.Object
   229  		now := time.Now()
   230  		if tc.jobName != "" {
   231  			job := &prowapi.ProwJob{
   232  				ObjectMeta: metav1.ObjectMeta{
   233  					Name:      "with-minimum_interval",
   234  					Namespace: "prowjobs",
   235  				},
   236  				Spec: prowapi.ProwJobSpec{
   237  					Type: prowapi.PeriodicJob,
   238  					Job:  tc.jobName,
   239  				},
   240  				Status: prowapi.ProwJobStatus{
   241  					StartTime: metav1.NewTime(now.Add(-tc.jobStartTimeAgo)),
   242  				},
   243  			}
   244  			jobCompleteTimeAgo := time.Millisecond
   245  			if tc.jobCompleteTimeAgo != 0 {
   246  				jobCompleteTimeAgo = tc.jobCompleteTimeAgo
   247  			}
   248  			complete := metav1.NewTime(now.Add(-jobCompleteTimeAgo))
   249  			if tc.jobComplete {
   250  				job.Status.CompletionTime = &complete
   251  			}
   252  			jobs = append(jobs, job)
   253  		}
   254  		fakeProwJobClient := newCreateTrackingClient(jobs)
   255  		fc := &fakeCron{}
   256  		if err := sync(fakeProwJobClient, &cfg, fc, now); err != nil {
   257  			t.Fatalf("For case %s, didn't expect error: %v", tc.testName, err)
   258  		}
   259  
   260  		sawCreation := fakeProwJobClient.sawCreate
   261  		if tc.shouldStart != sawCreation {
   262  			t.Errorf("For case %s, did the wrong thing.", tc.testName)
   263  		}
   264  	}
   265  }
   266  
   267  // Test sync periodic job scheduled by cron.
   268  func TestSyncCron(t *testing.T) {
   269  	testcases := []struct {
   270  		testName         string
   271  		jobName          string
   272  		jobComplete      bool
   273  		shouldStart      bool
   274  		enableScheduling bool
   275  	}{
   276  		{
   277  			testName:    "no job",
   278  			shouldStart: true,
   279  		},
   280  		{
   281  			testName:    "job with other name",
   282  			jobName:     "not-j",
   283  			jobComplete: true,
   284  			shouldStart: true,
   285  		},
   286  		{
   287  			testName:    "job still running",
   288  			jobName:     "j",
   289  			jobComplete: false,
   290  			shouldStart: false,
   291  		},
   292  		{
   293  			testName:    "job finished",
   294  			jobName:     "j",
   295  			jobComplete: true,
   296  			shouldStart: true,
   297  		},
   298  		{
   299  			testName:         "no job",
   300  			shouldStart:      true,
   301  			enableScheduling: true,
   302  		},
   303  	}
   304  	for _, tc := range testcases {
   305  		cfg := config.Config{
   306  			ProwConfig: config.ProwConfig{
   307  				ProwJobNamespace: "prowjobs",
   308  				Scheduler:        config.Scheduler{Enabled: tc.enableScheduling},
   309  			},
   310  			JobConfig: config.JobConfig{
   311  				Periodics: []config.Periodic{{JobBase: config.JobBase{Name: "j"}, Cron: "@every 1m"}},
   312  			},
   313  		}
   314  
   315  		var jobs []client.Object
   316  		now := time.Now()
   317  		if tc.jobName != "" {
   318  			job := &prowapi.ProwJob{
   319  				ObjectMeta: metav1.ObjectMeta{
   320  					Name:      "with-cron",
   321  					Namespace: "prowjobs",
   322  				},
   323  				Spec: prowapi.ProwJobSpec{
   324  					Type: prowapi.PeriodicJob,
   325  					Job:  tc.jobName,
   326  				},
   327  				Status: prowapi.ProwJobStatus{
   328  					StartTime: metav1.NewTime(now.Add(-time.Hour)),
   329  				},
   330  			}
   331  			complete := metav1.NewTime(now.Add(-time.Millisecond))
   332  			if tc.jobComplete {
   333  				job.Status.CompletionTime = &complete
   334  			}
   335  			jobs = append(jobs, job)
   336  		}
   337  		fakeProwJobClient := newCreateTrackingClient(jobs)
   338  		fc := &fakeCron{}
   339  		if err := sync(fakeProwJobClient, &cfg, fc, now); err != nil {
   340  			t.Fatalf("For case %s, didn't expect error: %v", tc.testName, err)
   341  		}
   342  
   343  		sawCreation := fakeProwJobClient.sawCreate
   344  		if tc.shouldStart {
   345  			if tc.shouldStart != sawCreation {
   346  				t.Errorf("For case %s, did the wrong thing.", tc.testName)
   347  			}
   348  			if tc.enableScheduling {
   349  				for _, obj := range fakeProwJobClient.created {
   350  					if pj, isPJ := obj.(*prowapi.ProwJob); isPJ && pj.Status.State != prowapi.SchedulingState {
   351  						t.Errorf("expected state %s but got %s", prowapi.SchedulingState, pj.Status.State)
   352  					}
   353  				}
   354  			}
   355  		}
   356  	}
   357  }
   358  
   359  func TestFlags(t *testing.T) {
   360  	cases := []struct {
   361  		name     string
   362  		args     map[string]string
   363  		del      sets.Set[string]
   364  		expected func(*options)
   365  		err      bool
   366  	}{
   367  		{
   368  			name: "minimal flags work",
   369  			expected: func(o *options) {
   370  				o.controllerManager.TimeoutListingProwJobs = 60 * time.Second
   371  				o.controllerManager.TimeoutListingProwJobsDefault = 60 * time.Second
   372  			},
   373  		},
   374  		{
   375  			name: "explicitly set --config-path",
   376  			args: map[string]string{
   377  				"--config-path": "/random/value",
   378  			},
   379  			expected: func(o *options) {
   380  				o.config.ConfigPath = "/random/value"
   381  				o.controllerManager.TimeoutListingProwJobs = 60 * time.Second
   382  				o.controllerManager.TimeoutListingProwJobsDefault = 60 * time.Second
   383  			},
   384  		},
   385  		{
   386  			name: "expicitly set --dry-run=false",
   387  			args: map[string]string{
   388  				"--dry-run": "false",
   389  			},
   390  			expected: func(o *options) {
   391  				o.dryRun = false
   392  				o.controllerManager.TimeoutListingProwJobs = 60 * time.Second
   393  				o.controllerManager.TimeoutListingProwJobsDefault = 60 * time.Second
   394  			},
   395  		},
   396  		{
   397  			name: "explicitly set --dry-run=true",
   398  			args: map[string]string{
   399  				"--dry-run": "true",
   400  			},
   401  			expected: func(o *options) {
   402  				o.dryRun = true
   403  				o.controllerManager.TimeoutListingProwJobs = 60 * time.Second
   404  				o.controllerManager.TimeoutListingProwJobsDefault = 60 * time.Second
   405  			},
   406  		},
   407  		{
   408  			name: "dry run defaults to true",
   409  			expected: func(o *options) {
   410  				o.dryRun = true
   411  				o.controllerManager.TimeoutListingProwJobs = 60 * time.Second
   412  				o.controllerManager.TimeoutListingProwJobsDefault = 60 * time.Second
   413  			},
   414  		},
   415  	}
   416  
   417  	for _, tc := range cases {
   418  		t.Run(tc.name, func(t *testing.T) {
   419  			expected := &options{
   420  				config: configflagutil.ConfigOptions{
   421  					ConfigPathFlagName:                    "config-path",
   422  					JobConfigPathFlagName:                 "job-config-path",
   423  					ConfigPath:                            "yo",
   424  					SupplementalProwConfigsFileNameSuffix: "_prowconfig.yaml",
   425  					InRepoConfigCacheSize:                 200,
   426  				},
   427  				dryRun:                 true,
   428  				instrumentationOptions: flagutil.DefaultInstrumentationOptions(),
   429  			}
   430  			if tc.expected != nil {
   431  				tc.expected(expected)
   432  			}
   433  
   434  			argMap := map[string]string{
   435  				"--config-path": "yo",
   436  			}
   437  			for k, v := range tc.args {
   438  				argMap[k] = v
   439  			}
   440  			for k := range tc.del {
   441  				delete(argMap, k)
   442  			}
   443  
   444  			var args []string
   445  			for k, v := range argMap {
   446  				args = append(args, k+"="+v)
   447  			}
   448  			fs := flag.NewFlagSet("fake-flags", flag.PanicOnError)
   449  			actual := gatherOptions(fs, args...)
   450  			switch err := actual.Validate(); {
   451  			case err != nil:
   452  				if !tc.err {
   453  					t.Errorf("unexpected error: %v", err)
   454  				}
   455  			case tc.err:
   456  				t.Errorf("failed to receive expected error")
   457  			case !reflect.DeepEqual(*expected, actual):
   458  				t.Errorf("%#v != expected %#v", actual, *expected)
   459  			}
   460  		})
   461  	}
   462  }
   463  
   464  type createTrackingClient struct {
   465  	ctrlruntimeclient.Client
   466  	sawCreate bool
   467  	created   []ctrlruntimeclient.Object
   468  }
   469  
   470  func (ct *createTrackingClient) Create(ctx context.Context, obj ctrlruntimeclient.Object, opts ...ctrlruntimeclient.CreateOption) error {
   471  	ct.sawCreate = true
   472  	ct.created = append(ct.created, obj)
   473  	return ct.Client.Create(ctx, obj, opts...)
   474  }
   475  
   476  func newCreateTrackingClient(objs []client.Object) *createTrackingClient {
   477  	return &createTrackingClient{
   478  		Client:  fakectrlruntimeclient.NewClientBuilder().WithObjects(objs...).Build(),
   479  		created: make([]ctrlruntimeclient.Object, 0),
   480  	}
   481  }