1
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 v1 "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
41
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
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
170 T1 := *topOfTheHour()
171
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
188
189 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
190
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
199
200 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
201
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
212
213 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
214
215 cj.Status.LastScheduleTime = &metav1.Time{Time: T1}
216
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
225
226 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(-10 * time.Minute)}
227
228 cj.Status.LastScheduleTime = &metav1.Time{Time: T1}
229
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
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
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
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
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
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
276 cj.ObjectMeta.CreationTimestamp = metav1.Time{Time: T1.Add(10 * time.Second)}
277 cj.Status.LastScheduleTime = nil
278 now := *deltaTimeAfterTopOfTheHour(1 * time.Hour)
279
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 TestIsJobSucceeded(t *testing.T) {
710 tests := map[string]struct {
711 job batchv1.Job
712 wantResult bool
713 }{
714 "job doesn't have any conditions": {
715 wantResult: false,
716 },
717 "job has Complete=True condition": {
718 job: batchv1.Job{
719 Status: batchv1.JobStatus{
720 Conditions: []batchv1.JobCondition{
721 {
722 Type: batchv1.JobSuspended,
723 Status: v1.ConditionFalse,
724 },
725 {
726 Type: batchv1.JobComplete,
727 Status: v1.ConditionTrue,
728 },
729 },
730 },
731 },
732 wantResult: true,
733 },
734 "job has Complete=False condition": {
735 job: batchv1.Job{
736 Status: batchv1.JobStatus{
737 Conditions: []batchv1.JobCondition{
738 {
739 Type: batchv1.JobFailed,
740 Status: v1.ConditionTrue,
741 },
742 {
743 Type: batchv1.JobComplete,
744 Status: v1.ConditionFalse,
745 },
746 },
747 },
748 },
749 wantResult: false,
750 },
751 }
752 for name, tc := range tests {
753 t.Run(name, func(t *testing.T) {
754 gotResult := IsJobSucceeded(&tc.job)
755 if tc.wantResult != gotResult {
756 t.Errorf("unexpected result, want=%v, got=%v", tc.wantResult, gotResult)
757 }
758 })
759 }
760 }
761
762 func topOfTheHour() *time.Time {
763 T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z")
764 if err != nil {
765 panic("test setup error")
766 }
767 return &T1
768 }
769
770 func deltaTimeAfterTopOfTheHour(duration time.Duration) *time.Time {
771 T1, err := time.Parse(time.RFC3339, "2016-05-19T10:00:00Z")
772 if err != nil {
773 panic("test setup error")
774 }
775 t := T1.Add(duration)
776 return &t
777 }
778
View as plain text