1
16
17 package ttlafterfinished
18
19 import (
20 "strings"
21 "testing"
22 "time"
23
24 batchv1 "k8s.io/api/batch/v1"
25 corev1 "k8s.io/api/core/v1"
26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27 "k8s.io/klog/v2"
28 "k8s.io/klog/v2/ktesting"
29 "k8s.io/utils/pointer"
30 )
31
32 func newJob(completionTime, failedTime metav1.Time, ttl *int32) *batchv1.Job {
33 j := &batchv1.Job{
34 TypeMeta: metav1.TypeMeta{Kind: "Job"},
35 ObjectMeta: metav1.ObjectMeta{
36 Name: "foobar",
37 Namespace: metav1.NamespaceDefault,
38 },
39 Spec: batchv1.JobSpec{
40 Selector: &metav1.LabelSelector{
41 MatchLabels: map[string]string{"foo": "bar"},
42 },
43 Template: corev1.PodTemplateSpec{
44 ObjectMeta: metav1.ObjectMeta{
45 Labels: map[string]string{
46 "foo": "bar",
47 },
48 },
49 Spec: corev1.PodSpec{
50 Containers: []corev1.Container{
51 {Image: "foo/bar"},
52 },
53 },
54 },
55 },
56 }
57
58 if !completionTime.IsZero() {
59 c := batchv1.JobCondition{Type: batchv1.JobComplete, Status: corev1.ConditionTrue, LastTransitionTime: completionTime}
60 j.Status.Conditions = append(j.Status.Conditions, c)
61 }
62
63 if !failedTime.IsZero() {
64 c := batchv1.JobCondition{Type: batchv1.JobFailed, Status: corev1.ConditionTrue, LastTransitionTime: failedTime}
65 j.Status.Conditions = append(j.Status.Conditions, c)
66 }
67
68 if ttl != nil {
69 j.Spec.TTLSecondsAfterFinished = ttl
70 }
71
72 return j
73 }
74
75 func TestTimeLeft(t *testing.T) {
76 now := metav1.Now()
77
78 testCases := []struct {
79 name string
80 completionTime metav1.Time
81 failedTime metav1.Time
82 ttl *int32
83 since *time.Time
84 expectErr bool
85 expectErrStr string
86 expectedTimeLeft *time.Duration
87 expectedExpireAt time.Time
88 }{
89 {
90 name: "Error case: Job unfinished",
91 ttl: pointer.Int32(100),
92 since: &now.Time,
93 expectErr: true,
94 expectErrStr: "should not be cleaned up",
95 },
96 {
97 name: "Error case: Job completed now, no TTL",
98 completionTime: now,
99 since: &now.Time,
100 expectErr: true,
101 expectErrStr: "should not be cleaned up",
102 },
103 {
104 name: "Job completed now, 0s TTL",
105 completionTime: now,
106 ttl: pointer.Int32(0),
107 since: &now.Time,
108 expectedTimeLeft: pointer.Duration(0 * time.Second),
109 expectedExpireAt: now.Time,
110 },
111 {
112 name: "Job completed now, 10s TTL",
113 completionTime: now,
114 ttl: pointer.Int32(10),
115 since: &now.Time,
116 expectedTimeLeft: pointer.Duration(10 * time.Second),
117 expectedExpireAt: now.Add(10 * time.Second),
118 },
119 {
120 name: "Job completed 10s ago, 15s TTL",
121 completionTime: metav1.NewTime(now.Add(-10 * time.Second)),
122 ttl: pointer.Int32(15),
123 since: &now.Time,
124 expectedTimeLeft: pointer.Duration(5 * time.Second),
125 expectedExpireAt: now.Add(5 * time.Second),
126 },
127 {
128 name: "Error case: Job failed now, no TTL",
129 failedTime: now,
130 since: &now.Time,
131 expectErr: true,
132 expectErrStr: "should not be cleaned up",
133 },
134 {
135 name: "Job failed now, 0s TTL",
136 failedTime: now,
137 ttl: pointer.Int32(0),
138 since: &now.Time,
139 expectedTimeLeft: pointer.Duration(0 * time.Second),
140 expectedExpireAt: now.Time,
141 },
142 {
143 name: "Job failed now, 10s TTL",
144 failedTime: now,
145 ttl: pointer.Int32(10),
146 since: &now.Time,
147 expectedTimeLeft: pointer.Duration(10 * time.Second),
148 expectedExpireAt: now.Add(10 * time.Second),
149 },
150 {
151 name: "Job failed 10s ago, 15s TTL",
152 failedTime: metav1.NewTime(now.Add(-10 * time.Second)),
153 ttl: pointer.Int32(15),
154 since: &now.Time,
155 expectedTimeLeft: pointer.Duration(5 * time.Second),
156 expectedExpireAt: now.Add(5 * time.Second),
157 },
158 }
159
160 for _, tc := range testCases {
161
162 job := newJob(tc.completionTime, tc.failedTime, tc.ttl)
163 _, ctx := ktesting.NewTestContext(t)
164 logger := klog.FromContext(ctx)
165 gotTimeLeft, gotExpireAt, gotErr := timeLeft(logger, job, tc.since)
166 if tc.expectErr != (gotErr != nil) {
167 t.Errorf("%s: expected error is %t, got %t, error: %v", tc.name, tc.expectErr, gotErr != nil, gotErr)
168 }
169 if tc.expectErr && len(tc.expectErrStr) == 0 {
170 t.Errorf("%s: invalid test setup; error message must not be empty for error cases", tc.name)
171 }
172 if tc.expectErr && !strings.Contains(gotErr.Error(), tc.expectErrStr) {
173 t.Errorf("%s: expected error message contains %q, got %v", tc.name, tc.expectErrStr, gotErr)
174 }
175 if !tc.expectErr {
176 if *gotTimeLeft != *tc.expectedTimeLeft {
177 t.Errorf("%s: expected time left %v, got %v", tc.name, tc.expectedTimeLeft, gotTimeLeft)
178 }
179 if *gotExpireAt != tc.expectedExpireAt {
180 t.Errorf("%s: expected expire at %v, got %v", tc.name, tc.expectedExpireAt, *gotExpireAt)
181 }
182 }
183 }
184 }
185
View as plain text