Skip to content

Commit 9f70d35

Browse files
committed
[task-controller] split Reconcile and other tests
1 parent 17ad2fb commit 9f70d35

2 files changed

Lines changed: 337 additions & 241 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/*
2+
* === This file is part of ALICE O² ===
3+
*
4+
* Copyright 2026 CERN and copyright holders of ALICE O².
5+
* Author: Michal Tichak <michal.tichak@cern.ch>
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*
20+
* In applying this license CERN does not waive the privileges and
21+
* immunities granted to it by virtue of its status as an
22+
* Intergovernmental Organization or submit itself to any jurisdiction.
23+
*/
24+
25+
package controller
26+
27+
import (
28+
"testing"
29+
30+
v1 "k8s.io/api/core/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/apimachinery/pkg/types"
34+
35+
aliecsv1alpha1 "github.com/AliceO2Group/Control/control-operator/api/v1alpha1"
36+
)
37+
38+
// newTestScheme is used to register owner of pod properly in TestPodForTask
39+
func newTestScheme(t *testing.T) *runtime.Scheme {
40+
t.Helper()
41+
scheme := runtime.NewScheme()
42+
if err := aliecsv1alpha1.AddToScheme(scheme); err != nil {
43+
t.Fatalf("failed to add aliecsv1alpha1 to scheme: %v", err)
44+
}
45+
return scheme
46+
}
47+
48+
func TestPodForTask(t *testing.T) {
49+
r := &TaskReconciler{Scheme: newTestScheme(t)}
50+
51+
task := &aliecsv1alpha1.Task{
52+
ObjectMeta: metav1.ObjectMeta{
53+
Name: "my-task",
54+
Namespace: "my-namespace",
55+
UID: types.UID("test-uid"),
56+
},
57+
Spec: aliecsv1alpha1.TaskSpec{
58+
Pod: v1.PodSpec{
59+
RestartPolicy: v1.RestartPolicyAlways,
60+
Containers: []v1.Container{
61+
{Name: "main", Image: "example.io/image:latest"},
62+
},
63+
},
64+
},
65+
}
66+
67+
pod := r.podForTask(task)
68+
69+
if pod.Name != "aliecs-task-pod-my-task" {
70+
t.Errorf("pod.Name = %q, want %q", pod.Name, "aliecs-task-pod-my-task")
71+
}
72+
if pod.Namespace != task.Namespace {
73+
t.Errorf("pod.Namespace = %q, want %q", pod.Namespace, task.Namespace)
74+
}
75+
76+
wantLabels := map[string]string{
77+
"task_name": "my-task",
78+
"application": "ControlOperator",
79+
}
80+
for k, v := range wantLabels {
81+
if got := pod.Labels[k]; got != v {
82+
t.Errorf("pod.Labels[%q] = %q, want %q", k, got, v)
83+
}
84+
}
85+
86+
if len(pod.Spec.Containers) != 1 || pod.Spec.Containers[0].Name != "main" {
87+
t.Errorf("pod.Spec.Containers not copied from task.Spec.Pod, got %+v", pod.Spec.Containers)
88+
}
89+
90+
// podForTask always forces RestartPolicyNever, regardless of what's set on the Task spec.
91+
if pod.Spec.RestartPolicy != v1.RestartPolicyNever {
92+
t.Errorf("pod.Spec.RestartPolicy = %q, want %q", pod.Spec.RestartPolicy, v1.RestartPolicyNever)
93+
}
94+
95+
ownerRefs := pod.GetOwnerReferences()
96+
if len(ownerRefs) != 1 {
97+
t.Fatalf("expected exactly one owner reference, got %d: %+v", len(ownerRefs), ownerRefs)
98+
}
99+
owner := ownerRefs[0]
100+
if owner.Kind != "Task" {
101+
t.Errorf("owner.Kind = %q, want %q", owner.Kind, "Task")
102+
}
103+
if owner.Name != task.Name {
104+
t.Errorf("owner.Name = %q, want %q", owner.Name, task.Name)
105+
}
106+
if owner.UID != task.UID {
107+
t.Errorf("owner.UID = %q, want %q", owner.UID, task.UID)
108+
}
109+
if owner.Controller == nil || !*owner.Controller {
110+
t.Errorf("owner.Controller = %v, want true", owner.Controller)
111+
}
112+
}
113+
114+
func TestPodNameFromTask(t *testing.T) {
115+
tests := []struct {
116+
name string
117+
taskName string
118+
want string
119+
}{
120+
{name: "simple task name", taskName: "my-task", want: "aliecs-task-pod-my-task"},
121+
{name: "empty task name", taskName: "", want: "aliecs-task-pod-"},
122+
}
123+
124+
for _, tt := range tests {
125+
t.Run(tt.name, func(t *testing.T) {
126+
if got := podNameFromTask(tt.taskName); got != tt.want {
127+
t.Errorf("podNameFromTask(%q) = %q, want %q", tt.taskName, got, tt.want)
128+
}
129+
})
130+
}
131+
}
132+
133+
func TestLabelsForTask(t *testing.T) {
134+
task := &aliecsv1alpha1.Task{
135+
ObjectMeta: metav1.ObjectMeta{
136+
Name: "my-task",
137+
},
138+
}
139+
140+
want := map[string]string{
141+
"task_name": "my-task",
142+
"application": "ControlOperator",
143+
}
144+
145+
got := labelsForTask(task)
146+
147+
if len(got) != len(want) {
148+
t.Fatalf("labelsForTask() = %v, want %v", got, want)
149+
}
150+
for k, v := range want {
151+
if got[k] != v {
152+
t.Errorf("labelsForTask()[%q] = %q, want %q", k, got[k], v)
153+
}
154+
}
155+
}
156+
157+
func TestIsPodFailed(t *testing.T) {
158+
tests := []struct {
159+
name string
160+
pod *v1.Pod
161+
wantFailed bool
162+
}{
163+
{
164+
name: "running pod with ready container is not failed",
165+
pod: &v1.Pod{
166+
Status: v1.PodStatus{
167+
Phase: v1.PodRunning,
168+
ContainerStatuses: []v1.ContainerStatus{{Name: "main", Ready: true}},
169+
},
170+
},
171+
wantFailed: false,
172+
},
173+
{
174+
name: "pod phase Failed is failed",
175+
pod: &v1.Pod{
176+
Status: v1.PodStatus{
177+
Phase: v1.PodFailed,
178+
Reason: "Evicted",
179+
},
180+
},
181+
wantFailed: true,
182+
},
183+
{
184+
name: "container waiting CrashLoopBackOff is failed",
185+
pod: &v1.Pod{
186+
Status: v1.PodStatus{
187+
Phase: v1.PodRunning,
188+
ContainerStatuses: []v1.ContainerStatus{{
189+
Name: "main",
190+
State: v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "CrashLoopBackOff"}},
191+
}},
192+
},
193+
},
194+
wantFailed: true,
195+
},
196+
{
197+
name: "container waiting for a benign reason is not failed",
198+
pod: &v1.Pod{
199+
Status: v1.PodStatus{
200+
Phase: v1.PodRunning,
201+
ContainerStatuses: []v1.ContainerStatus{{
202+
Name: "main",
203+
State: v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "ContainerCreating"}},
204+
}},
205+
},
206+
},
207+
wantFailed: false,
208+
},
209+
{
210+
name: "container terminated with non-zero exit code is failed",
211+
pod: &v1.Pod{
212+
Status: v1.PodStatus{
213+
Phase: v1.PodRunning,
214+
ContainerStatuses: []v1.ContainerStatus{{
215+
Name: "main",
216+
State: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{ExitCode: 1}},
217+
}},
218+
},
219+
},
220+
wantFailed: true,
221+
},
222+
{
223+
name: "container terminated with exit code 0 is not failed",
224+
pod: &v1.Pod{
225+
Status: v1.PodStatus{
226+
Phase: v1.PodRunning,
227+
ContainerStatuses: []v1.ContainerStatus{{
228+
Name: "main",
229+
State: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{ExitCode: 0}},
230+
}},
231+
},
232+
},
233+
wantFailed: false,
234+
},
235+
{
236+
name: "container with more than 3 restarts and not ready is failed",
237+
pod: &v1.Pod{
238+
Status: v1.PodStatus{
239+
Phase: v1.PodRunning,
240+
ContainerStatuses: []v1.ContainerStatus{{Name: "main", RestartCount: 4, Ready: false}},
241+
},
242+
},
243+
wantFailed: true,
244+
},
245+
{
246+
name: "container with more than 3 restarts but ready is not failed",
247+
pod: &v1.Pod{
248+
Status: v1.PodStatus{
249+
Phase: v1.PodRunning,
250+
ContainerStatuses: []v1.ContainerStatus{{Name: "main", RestartCount: 4, Ready: true}},
251+
},
252+
},
253+
wantFailed: false,
254+
},
255+
{
256+
name: "init container failure is failed",
257+
pod: &v1.Pod{
258+
Status: v1.PodStatus{
259+
Phase: v1.PodRunning,
260+
InitContainerStatuses: []v1.ContainerStatus{{
261+
Name: "init",
262+
State: v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "InvalidImageName"}},
263+
}},
264+
},
265+
},
266+
wantFailed: true,
267+
},
268+
}
269+
270+
for _, tt := range tests {
271+
t.Run(tt.name, func(t *testing.T) {
272+
gotFailed, gotReason := isPodFailed(tt.pod)
273+
if gotFailed != tt.wantFailed {
274+
t.Errorf("isPodFailed() failed = %v, want %v (reason: %q)", gotFailed, tt.wantFailed, gotReason)
275+
}
276+
if gotFailed && gotReason == "" {
277+
t.Errorf("isPodFailed() returned failed=true with empty reason")
278+
}
279+
if !gotFailed && gotReason != "" {
280+
t.Errorf("isPodFailed() returned failed=false with non-empty reason %q", gotReason)
281+
}
282+
})
283+
}
284+
}

0 commit comments

Comments
 (0)