k8s.io/kubernetes@v1.29.3/pkg/controller/cronjob/utils_test.go (about) 1 /* 2 Copyright 2016 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 cronjob 18 19 import ( 20 "reflect" 21 "sort" 22 "strings" 23 "testing" 24 "time" 25 26 cron "github.com/robfig/cron/v3" 27 batchv1 "k8s.io/api/batch/v1" 28 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/types" 31 utilfeature "k8s.io/apiserver/pkg/util/feature" 32 "k8s.io/client-go/tools/record" 33 featuregatetesting "k8s.io/component-base/featuregate/testing" 34 "k8s.io/klog/v2/ktesting" 35 "k8s.io/kubernetes/pkg/features" 36 "k8s.io/utils/pointer" 37 ) 38 39 func TestGetJobFromTemplate2(t *testing.T) { 40 // getJobFromTemplate2() needs to take the job template and copy the labels and annotations 41 // and other fields, and add a created-by reference. 42 var ( 43 one int64 = 1 44 no bool 45 timeZoneUTC = "UTC" 46 timeZoneCorrect = "Europe/Rome" 47 scheduledTime = *topOfTheHour() 48 ) 49 50 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CronJobsScheduledAnnotation, true)() 51 52 cj := batchv1.CronJob{ 53 ObjectMeta: metav1.ObjectMeta{ 54 Name: "mycronjob", 55 Namespace: "snazzycats", 56 UID: types.UID("1a2b3c"), 57 }, 58 Spec: batchv1.CronJobSpec{ 59 Schedule: "* * * * ?", 60 ConcurrencyPolicy: batchv1.AllowConcurrent, 61 JobTemplate: batchv1.JobTemplateSpec{ 62 ObjectMeta: metav1.ObjectMeta{ 63 CreationTimestamp: metav1.Time{Time: scheduledTime}, 64 Labels: map[string]string{"a": "b"}, 65 }, 66 Spec: batchv1.JobSpec{ 67 ActiveDeadlineSeconds: &one, 68 ManualSelector: &no, 69 Template: v1.PodTemplateSpec{ 70 ObjectMeta: metav1.ObjectMeta{ 71 Labels: map[string]string{ 72 "foo": "bar", 73 }, 74 }, 75 Spec: v1.PodSpec{ 76 Containers: []v1.Container{ 77 {Image: "foo/bar"}, 78 }, 79 }, 80 }, 81 }, 82 }, 83 }, 84 } 85 86 testCases := []struct { 87 name string 88 timeZone *string 89 inputAnnotations map[string]string 90 expectedScheduledTime func() time.Time 91 expectedNumberOfAnnotations int 92 }{ 93 { 94 name: "UTC timezone and one annotation", 95 timeZone: &timeZoneUTC, 96 inputAnnotations: map[string]string{"x": "y"}, 97 expectedScheduledTime: func() time.Time { 98 return scheduledTime 99 }, 100 expectedNumberOfAnnotations: 2, 101 }, 102 { 103 name: "nil timezone and one annotation", 104 timeZone: nil, 105 inputAnnotations: map[string]string{"x": "y"}, 106 expectedScheduledTime: func() time.Time { 107 return scheduledTime 108 }, 109 expectedNumberOfAnnotations: 2, 110 }, 111 { 112 name: "correct timezone and multiple annotation", 113 timeZone: &timeZoneCorrect, 114 inputAnnotations: map[string]string{"x": "y", "z": "x"}, 115 expectedScheduledTime: func() time.Time { 116 location, _ := time.LoadLocation(timeZoneCorrect) 117 return scheduledTime.In(location) 118 }, 119 expectedNumberOfAnnotations: 3, 120 }, 121 } 122 123 for _, tt := range testCases { 124 t.Run(tt.name, func(t *testing.T) { 125 cj.Spec.JobTemplate.Annotations = tt.inputAnnotations 126 cj.Spec.TimeZone = tt.timeZone 127 128 var job *batchv1.Job 129 job, err := getJobFromTemplate2(&cj, scheduledTime) 130 if err != nil { 131 t.Errorf("Did not expect error: %s", err) 132 } 133 if !strings.HasPrefix(job.ObjectMeta.Name, "mycronjob-") { 134 t.Errorf("Wrong Name") 135 } 136 if len(job.ObjectMeta.Labels) != 1 { 137 t.Errorf("Wrong number of labels") 138 } 139 if len(job.ObjectMeta.Annotations) != tt.expectedNumberOfAnnotations { 140 t.Errorf("Wrong number of annotations") 141 } 142 143 scheduledAnnotation := job.ObjectMeta.Annotations[batchv1.CronJobScheduledTimestampAnnotation] 144 timeZoneLocation, err := time.LoadLocation(pointer.StringDeref(tt.timeZone, "")) 145 if err != nil { 146 t.Errorf("Wrong timezone location") 147 } 148 if len(job.ObjectMeta.Annotations) != 0 && scheduledAnnotation != tt.expectedScheduledTime().Format(time.RFC3339) { 149 t.Errorf("Wrong cronJob scheduled timestamp annotation, expexted %s, got %s.", tt.expectedScheduledTime().In(timeZoneLocation).Format(time.RFC3339), scheduledAnnotation) 150 } 151 }) 152 } 153 } 154 155 func TestNextScheduleTime(t *testing.T) { 156 logger, _ := ktesting.NewTestContext(t) 157 // schedule is hourly on the hour 158 schedule := "0 * * * ?" 159 160 ParseSchedule := func(schedule string) cron.Schedule { 161 sched, err := cron.ParseStandard(schedule) 162 if err != nil { 163 t.Errorf("Error parsing schedule: %#v", err) 164 return nil 165 } 166 return sched 167 } 168 recorder := record.NewFakeRecorder(50) 169 // T1 is a scheduled start time of that schedule 170 T1 := *topOfTheHour() 171 // T2 is a scheduled start time of that schedule after T1 172 T2 := *deltaTimeAfterTopOfTheHour(1 * time.Hour) 173 174 cj := batchv1.CronJob{ 175 ObjectMeta: metav1.ObjectMeta{ 176 Name: "mycronjob", 177 Namespace: metav1.NamespaceDefault, 178 UID: types.UID("1a2b3c"), 179 }, 180 Spec: batchv1.CronJobSpec{ 181 Schedule: schedule, 182 ConcurrencyPolicy: batchv1.AllowConcurrent, 183 JobTemplate: batchv1.JobTemplateSpec{}, 184 }, 185 } 186 { 187 // Case 1: no known start times, and none needed yet. 188 // Creation time is before T1. 189 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)} 190 // Current time is more than creation time, but less than T1. 191 now := T1.Add(-7 * time.Minute) 192 schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder) 193 if schedule != nil { 194 t.Errorf("expected no start time, got: %v", schedule) 195 } 196 } 197 { 198 // Case 2: no known start times, and one needed. 199 // Creation time is before T1. 200 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)} 201 // Current time is after T1 202 now := T1.Add(2 * time.Second) 203 schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder) 204 if schedule == nil { 205 t.Errorf("expected 1 start time, got nil") 206 } else if !schedule.Equal(T1) { 207 t.Errorf("expected: %v, got: %v", T1, schedule) 208 } 209 } 210 { 211 // Case 3: known LastScheduleTime, no start needed. 212 // Creation time is before T1. 213 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)} 214 // Status shows a start at the expected time. 215 cj.Status.LastScheduleTime = &metav1.Time{Time: T1} 216 // Current time is after T1 217 now := T1.Add(2 * time.Minute) 218 schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder) 219 if schedule != nil { 220 t.Errorf("expected 0 start times, got: %v", schedule) 221 } 222 } 223 { 224 // Case 4: known LastScheduleTime, a start needed 225 // Creation time is before T1. 226 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)} 227 // Status shows a start at the expected time. 228 cj.Status.LastScheduleTime = &metav1.Time{Time: T1} 229 // Current time is after T1 and after T2 230 now := T2.Add(5 * time.Minute) 231 schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder) 232 if schedule == nil { 233 t.Errorf("expected 1 start times, got nil") 234 } else if !schedule.Equal(T2) { 235 t.Errorf("expected: %v, got: %v", T2, schedule) 236 } 237 } 238 { 239 // Case 5: known LastScheduleTime, two starts needed 240 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)} 241 cj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)} 242 // Current time is after T1 and after T2 243 now := T2.Add(5 * time.Minute) 244 schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder) 245 if schedule == nil { 246 t.Errorf("expected 1 start times, got nil") 247 } else if !schedule.Equal(T2) { 248 t.Errorf("expected: %v, got: %v", T2, schedule) 249 } 250 } 251 { 252 // Case 6: now is way way ahead of last start time, and there is no deadline. 253 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)} 254 cj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)} 255 now := T2.Add(10 * 24 * time.Hour) 256 schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder) 257 if schedule == nil { 258 t.Errorf("expected more than 0 missed times") 259 } 260 } 261 { 262 // Case 7: now is way way ahead of last start time, but there is a short deadline. 263 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-2 * time.Hour)} 264 cj.Status.LastScheduleTime = &metav1.Time{Time: T1.Add(-1 * time.Hour)} 265 now := T2.Add(10 * 24 * time.Hour) 266 // Deadline is short 267 deadline := int64(2 * 60 * 60) 268 cj.Spec.StartingDeadlineSeconds = &deadline 269 schedule, _ := nextScheduleTime(logger, &cj, now, ParseSchedule(cj.Spec.Schedule), recorder) 270 if schedule == nil { 271 t.Errorf("expected more than 0 missed times") 272 } 273 } 274 { 275 // Case 8: ensure the error from mostRecentScheduleTime gets populated up 276 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(10 * time.Second)} 277 cj.Status.LastScheduleTime = nil 278 now := *deltaTimeAfterTopOfTheHour(1 * time.Hour) 279 // rouge schedule 280 schedule, err := nextScheduleTime(logger, &cj, now, ParseSchedule("59 23 31 2 *"), recorder) 281 if schedule != nil { 282 t.Errorf("expected no start time, got: %v", schedule) 283 } 284 if err == nil { 285 t.Errorf("expected error") 286 } 287 } 288 } 289 290 func TestByJobStartTime(t *testing.T) { 291 now := metav1.NewTime(time.Date(2018, time.January, 1, 2, 3, 4, 5, time.UTC)) 292 later := metav1.NewTime(time.Date(2019, time.January, 1, 2, 3, 4, 5, time.UTC)) 293 aNil := &batchv1.Job{ 294 ObjectMeta: metav1.ObjectMeta{Name: "a"}, 295 Status: batchv1.JobStatus{}, 296 } 297 bNil := &batchv1.Job{ 298 ObjectMeta: metav1.ObjectMeta{Name: "b"}, 299 Status: batchv1.JobStatus{}, 300 } 301 aSet := &batchv1.Job{ 302 ObjectMeta: metav1.ObjectMeta{Name: "a"}, 303 Status: batchv1.JobStatus{StartTime: &now}, 304 } 305 bSet := &batchv1.Job{ 306 ObjectMeta: metav1.ObjectMeta{Name: "b"}, 307 Status: batchv1.JobStatus{StartTime: &now}, 308 } 309 aSetLater := &batchv1.Job{ 310 ObjectMeta: metav1.ObjectMeta{Name: "a"}, 311 Status: batchv1.JobStatus{StartTime: &later}, 312 } 313 314 testCases := []struct { 315 name string 316 input, expected []*batchv1.Job 317 }{ 318 { 319 name: "both have nil start times", 320 input: []*batchv1.Job{bNil, aNil}, 321 expected: []*batchv1.Job{aNil, bNil}, 322 }, 323 { 324 name: "only the first has a nil start time", 325 input: []*batchv1.Job{aNil, bSet}, 326 expected: []*batchv1.Job{bSet, aNil}, 327 }, 328 { 329 name: "only the second has a nil start time", 330 input: []*batchv1.Job{aSet, bNil}, 331 expected: []*batchv1.Job{aSet, bNil}, 332 }, 333 { 334 name: "both have non-nil, equal start time", 335 input: []*batchv1.Job{bSet, aSet}, 336 expected: []*batchv1.Job{aSet, bSet}, 337 }, 338 { 339 name: "both have non-nil, different start time", 340 input: []*batchv1.Job{aSetLater, bSet}, 341 expected: []*batchv1.Job{bSet, aSetLater}, 342 }, 343 } 344 345 for _, testCase := range testCases { 346 sort.Sort(byJobStartTime(testCase.input)) 347 if !reflect.DeepEqual(testCase.input, testCase.expected) { 348 t.Errorf("case: '%s', jobs not sorted as expected", testCase.name) 349 } 350 } 351 } 352 353 func TestMostRecentScheduleTime(t *testing.T) { 354 metav1TopOfTheHour := metav1.NewTime(*topOfTheHour()) 355 metav1HalfPastTheHour := metav1.NewTime(*deltaTimeAfterTopOfTheHour(30 * time.Minute)) 356 metav1MinuteAfterTopOfTheHour := metav1.NewTime(*deltaTimeAfterTopOfTheHour(1 * time.Minute)) 357 oneMinute := int64(60) 358 tenSeconds := int64(10) 359 360 tests := []struct { 361 name string 362 cj *batchv1.CronJob 363 includeSDS bool 364 now time.Time 365 expectedEarliestTime time.Time 366 expectedRecentTime *time.Time 367 expectedTooManyMissed missedSchedulesType 368 wantErr bool 369 }{ 370 { 371 name: "now before next schedule", 372 cj: &batchv1.CronJob{ 373 ObjectMeta: metav1.ObjectMeta{ 374 CreationTimestamp: metav1TopOfTheHour, 375 }, 376 Spec: batchv1.CronJobSpec{ 377 Schedule: "0 * * * *", 378 }, 379 }, 380 now: topOfTheHour().Add(30 * time.Second), 381 expectedRecentTime: nil, 382 expectedEarliestTime: *topOfTheHour(), 383 }, 384 { 385 name: "now just after next schedule", 386 cj: &batchv1.CronJob{ 387 ObjectMeta: metav1.ObjectMeta{ 388 CreationTimestamp: metav1TopOfTheHour, 389 }, 390 Spec: batchv1.CronJobSpec{ 391 Schedule: "0 * * * *", 392 }, 393 }, 394 now: topOfTheHour().Add(61 * time.Minute), 395 expectedRecentTime: deltaTimeAfterTopOfTheHour(60 * time.Minute), 396 expectedEarliestTime: *topOfTheHour(), 397 }, 398 { 399 name: "missed 5 schedules", 400 cj: &batchv1.CronJob{ 401 ObjectMeta: metav1.ObjectMeta{ 402 CreationTimestamp: metav1.NewTime(*deltaTimeAfterTopOfTheHour(10 * time.Second)), 403 }, 404 Spec: batchv1.CronJobSpec{ 405 Schedule: "0 * * * *", 406 }, 407 }, 408 now: *deltaTimeAfterTopOfTheHour(301 * time.Minute), 409 expectedRecentTime: deltaTimeAfterTopOfTheHour(300 * time.Minute), 410 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(10 * time.Second), 411 expectedTooManyMissed: fewMissed, 412 }, 413 { 414 name: "complex schedule", 415 cj: &batchv1.CronJob{ 416 ObjectMeta: metav1.ObjectMeta{ 417 CreationTimestamp: metav1TopOfTheHour, 418 }, 419 Spec: batchv1.CronJobSpec{ 420 Schedule: "30 6-16/4 * * 1-5", 421 }, 422 Status: batchv1.CronJobStatus{ 423 LastScheduleTime: &metav1HalfPastTheHour, 424 }, 425 }, 426 now: *deltaTimeAfterTopOfTheHour(24*time.Hour + 31*time.Minute), 427 expectedRecentTime: deltaTimeAfterTopOfTheHour(24*time.Hour + 30*time.Minute), 428 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(30 * time.Minute), 429 expectedTooManyMissed: fewMissed, 430 }, 431 { 432 name: "another complex schedule", 433 cj: &batchv1.CronJob{ 434 ObjectMeta: metav1.ObjectMeta{ 435 CreationTimestamp: metav1TopOfTheHour, 436 }, 437 Spec: batchv1.CronJobSpec{ 438 Schedule: "30 10,11,12 * * 1-5", 439 }, 440 Status: batchv1.CronJobStatus{ 441 LastScheduleTime: &metav1HalfPastTheHour, 442 }, 443 }, 444 now: *deltaTimeAfterTopOfTheHour(30*time.Hour + 30*time.Minute), 445 expectedRecentTime: nil, 446 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(30 * time.Minute), 447 expectedTooManyMissed: fewMissed, 448 }, 449 { 450 name: "complex schedule with longer diff between executions", 451 cj: &batchv1.CronJob{ 452 ObjectMeta: metav1.ObjectMeta{ 453 CreationTimestamp: metav1TopOfTheHour, 454 }, 455 Spec: batchv1.CronJobSpec{ 456 Schedule: "30 6-16/4 * * 1-5", 457 }, 458 Status: batchv1.CronJobStatus{ 459 LastScheduleTime: &metav1HalfPastTheHour, 460 }, 461 }, 462 now: *deltaTimeAfterTopOfTheHour(96*time.Hour + 31*time.Minute), 463 expectedRecentTime: deltaTimeAfterTopOfTheHour(96*time.Hour + 30*time.Minute), 464 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(30 * time.Minute), 465 expectedTooManyMissed: fewMissed, 466 }, 467 { 468 name: "complex schedule with shorter diff between executions", 469 cj: &batchv1.CronJob{ 470 ObjectMeta: metav1.ObjectMeta{ 471 CreationTimestamp: metav1TopOfTheHour, 472 }, 473 Spec: batchv1.CronJobSpec{ 474 Schedule: "30 6-16/4 * * 1-5", 475 }, 476 }, 477 now: *deltaTimeAfterTopOfTheHour(24*time.Hour + 31*time.Minute), 478 expectedRecentTime: deltaTimeAfterTopOfTheHour(24*time.Hour + 30*time.Minute), 479 expectedEarliestTime: *topOfTheHour(), 480 expectedTooManyMissed: fewMissed, 481 }, 482 { 483 name: "@every schedule", 484 cj: &batchv1.CronJob{ 485 ObjectMeta: metav1.ObjectMeta{ 486 CreationTimestamp: metav1.NewTime(*deltaTimeAfterTopOfTheHour(-59 * time.Minute)), 487 }, 488 Spec: batchv1.CronJobSpec{ 489 Schedule: "@every 1h", 490 StartingDeadlineSeconds: &tenSeconds, 491 }, 492 Status: batchv1.CronJobStatus{ 493 LastScheduleTime: &metav1MinuteAfterTopOfTheHour, 494 }, 495 }, 496 now: *deltaTimeAfterTopOfTheHour(7 * 24 * time.Hour), 497 expectedRecentTime: deltaTimeAfterTopOfTheHour((6 * 24 * time.Hour) + 23*time.Hour + 1*time.Minute), 498 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(1 * time.Minute), 499 expectedTooManyMissed: manyMissed, 500 }, 501 { 502 name: "rogue cronjob", 503 cj: &batchv1.CronJob{ 504 ObjectMeta: metav1.ObjectMeta{ 505 CreationTimestamp: metav1.NewTime(*deltaTimeAfterTopOfTheHour(10 * time.Second)), 506 }, 507 Spec: batchv1.CronJobSpec{ 508 Schedule: "59 23 31 2 *", 509 }, 510 }, 511 now: *deltaTimeAfterTopOfTheHour(1 * time.Hour), 512 expectedRecentTime: nil, 513 wantErr: true, 514 }, 515 { 516 name: "earliestTime being CreationTimestamp and LastScheduleTime", 517 cj: &batchv1.CronJob{ 518 ObjectMeta: metav1.ObjectMeta{ 519 CreationTimestamp: metav1TopOfTheHour, 520 }, 521 Spec: batchv1.CronJobSpec{ 522 Schedule: "0 * * * *", 523 }, 524 Status: batchv1.CronJobStatus{ 525 LastScheduleTime: &metav1TopOfTheHour, 526 }, 527 }, 528 now: *deltaTimeAfterTopOfTheHour(30 * time.Second), 529 expectedEarliestTime: *topOfTheHour(), 530 expectedRecentTime: nil, 531 }, 532 { 533 name: "earliestTime being LastScheduleTime", 534 cj: &batchv1.CronJob{ 535 ObjectMeta: metav1.ObjectMeta{ 536 CreationTimestamp: metav1TopOfTheHour, 537 }, 538 Spec: batchv1.CronJobSpec{ 539 Schedule: "*/5 * * * *", 540 }, 541 Status: batchv1.CronJobStatus{ 542 LastScheduleTime: &metav1HalfPastTheHour, 543 }, 544 }, 545 now: *deltaTimeAfterTopOfTheHour(31 * time.Minute), 546 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(30 * time.Minute), 547 expectedRecentTime: nil, 548 }, 549 { 550 name: "earliestTime being LastScheduleTime (within StartingDeadlineSeconds)", 551 cj: &batchv1.CronJob{ 552 ObjectMeta: metav1.ObjectMeta{ 553 CreationTimestamp: metav1TopOfTheHour, 554 }, 555 Spec: batchv1.CronJobSpec{ 556 Schedule: "*/5 * * * *", 557 StartingDeadlineSeconds: &oneMinute, 558 }, 559 Status: batchv1.CronJobStatus{ 560 LastScheduleTime: &metav1HalfPastTheHour, 561 }, 562 }, 563 now: *deltaTimeAfterTopOfTheHour(31 * time.Minute), 564 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(30 * time.Minute), 565 expectedRecentTime: nil, 566 }, 567 { 568 name: "earliestTime being LastScheduleTime (outside StartingDeadlineSeconds)", 569 cj: &batchv1.CronJob{ 570 ObjectMeta: metav1.ObjectMeta{ 571 CreationTimestamp: metav1TopOfTheHour, 572 }, 573 Spec: batchv1.CronJobSpec{ 574 Schedule: "*/5 * * * *", 575 StartingDeadlineSeconds: &oneMinute, 576 }, 577 Status: batchv1.CronJobStatus{ 578 LastScheduleTime: &metav1HalfPastTheHour, 579 }, 580 }, 581 includeSDS: true, 582 now: *deltaTimeAfterTopOfTheHour(32 * time.Minute), 583 expectedEarliestTime: *deltaTimeAfterTopOfTheHour(31 * time.Minute), 584 expectedRecentTime: nil, 585 }, 586 } 587 for _, tt := range tests { 588 t.Run(tt.name, func(t *testing.T) { 589 sched, err := cron.ParseStandard(tt.cj.Spec.Schedule) 590 if err != nil { 591 t.Errorf("error setting up the test, %s", err) 592 } 593 gotEarliestTime, gotRecentTime, gotTooManyMissed, err := mostRecentScheduleTime(tt.cj, tt.now, sched, tt.includeSDS) 594 if tt.wantErr { 595 if err == nil { 596 t.Error("mostRecentScheduleTime() got no error when expected one") 597 } 598 return 599 } 600 if !tt.wantErr && err != nil { 601 t.Error("mostRecentScheduleTime() got error when none expected") 602 } 603 if gotEarliestTime.IsZero() { 604 t.Errorf("earliestTime should never be 0, want %v", tt.expectedEarliestTime) 605 } 606 if !gotEarliestTime.Equal(tt.expectedEarliestTime) { 607 t.Errorf("expectedEarliestTime - got %v, want %v", gotEarliestTime, tt.expectedEarliestTime) 608 } 609 if !reflect.DeepEqual(gotRecentTime, tt.expectedRecentTime) { 610 t.Errorf("expectedRecentTime - got %v, want %v", gotRecentTime, tt.expectedRecentTime) 611 } 612 if gotTooManyMissed != tt.expectedTooManyMissed { 613 t.Errorf("expectedNumberOfMisses - got %v, want %v", gotTooManyMissed, tt.expectedTooManyMissed) 614 } 615 }) 616 } 617 } 618 619 func TestNextScheduleTimeDuration(t *testing.T) { 620 metav1TopOfTheHour := metav1.NewTime(*topOfTheHour()) 621 metav1HalfPastTheHour := metav1.NewTime(*deltaTimeAfterTopOfTheHour(30 * time.Minute)) 622 metav1TwoHoursLater := metav1.NewTime(*deltaTimeAfterTopOfTheHour(2 * time.Hour)) 623 624 tests := []struct { 625 name string 626 cj *batchv1.CronJob 627 now time.Time 628 expectedDuration time.Duration 629 }{ 630 { 631 name: "complex schedule skipping weekend", 632 cj: &batchv1.CronJob{ 633 ObjectMeta: metav1.ObjectMeta{ 634 CreationTimestamp: metav1TopOfTheHour, 635 }, 636 Spec: batchv1.CronJobSpec{ 637 Schedule: "30 6-16/4 * * 1-5", 638 }, 639 Status: batchv1.CronJobStatus{ 640 LastScheduleTime: &metav1HalfPastTheHour, 641 }, 642 }, 643 now: *deltaTimeAfterTopOfTheHour(24*time.Hour + 31*time.Minute), 644 expectedDuration: 3*time.Hour + 59*time.Minute + nextScheduleDelta, 645 }, 646 { 647 name: "another complex schedule skipping weekend", 648 cj: &batchv1.CronJob{ 649 ObjectMeta: metav1.ObjectMeta{ 650 CreationTimestamp: metav1TopOfTheHour, 651 }, 652 Spec: batchv1.CronJobSpec{ 653 Schedule: "30 10,11,12 * * 1-5", 654 }, 655 Status: batchv1.CronJobStatus{ 656 LastScheduleTime: &metav1HalfPastTheHour, 657 }, 658 }, 659 now: *deltaTimeAfterTopOfTheHour(30*time.Hour + 30*time.Minute), 660 expectedDuration: 66*time.Hour + nextScheduleDelta, 661 }, 662 { 663 name: "once a week cronjob, missed two runs", 664 cj: &batchv1.CronJob{ 665 ObjectMeta: metav1.ObjectMeta{ 666 CreationTimestamp: metav1TopOfTheHour, 667 }, 668 Spec: batchv1.CronJobSpec{ 669 Schedule: "0 12 * * 4", 670 }, 671 Status: batchv1.CronJobStatus{ 672 LastScheduleTime: &metav1TwoHoursLater, 673 }, 674 }, 675 now: *deltaTimeAfterTopOfTheHour(19*24*time.Hour + 1*time.Hour + 30*time.Minute), 676 expectedDuration: 48*time.Hour + 30*time.Minute + nextScheduleDelta, 677 }, 678 { 679 name: "no previous run of a cronjob", 680 cj: &batchv1.CronJob{ 681 ObjectMeta: metav1.ObjectMeta{ 682 CreationTimestamp: metav1TopOfTheHour, 683 }, 684 Spec: batchv1.CronJobSpec{ 685 Schedule: "0 12 * * 5", 686 }, 687 }, 688 now: *deltaTimeAfterTopOfTheHour(6 * time.Hour), 689 expectedDuration: 20*time.Hour + nextScheduleDelta, 690 }, 691 } 692 for _, tt := range tests { 693 t.Run(tt.name, func(t *testing.T) { 694 sched, err := cron.ParseStandard(tt.cj.Spec.Schedule) 695 if err != nil { 696 t.Errorf("error setting up the test, %s", err) 697 } 698 gotScheduleTimeDuration := nextScheduleTimeDuration(tt.cj, tt.now, sched) 699 if *gotScheduleTimeDuration < 0 { 700 t.Errorf("scheduleTimeDuration should never be less than 0, got %s", gotScheduleTimeDuration) 701 } 702 if !reflect.DeepEqual(gotScheduleTimeDuration, &tt.expectedDuration) { 703 t.Errorf("scheduleTimeDuration - got %s, want %s", gotScheduleTimeDuration, tt.expectedDuration) 704 } 705 }) 706 } 707 } 708 709 func topOfTheHour() *time.Time { 710 T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z") 711 if err != nil { 712 panic("test setup error") 713 } 714 return &T1 715 } 716 717 func deltaTimeAfterTopOfTheHour(duration time.Duration) *time.Time { 718 T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z") 719 if err != nil { 720 panic("test setup error") 721 } 722 t := T1.Add(duration) 723 return &t 724 }