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 }