@@ -18,6 +18,7 @@ package taskrun
1818
1919import (
2020 "testing"
21+ "time"
2122
2223 "github.com/google/go-cmp/cmp"
2324 "github.com/tektoncd/pipeline/pkg/apis/config"
@@ -37,6 +38,125 @@ import (
3738 _ "knative.dev/pkg/system/testing" // Setup system.Namespace()
3839)
3940
41+ // TestReconcileTimeoutWithEmptyStepStatus tests that when a TaskRun times out
42+ // and tr.Status.Steps is empty (race condition), the reconciler populates
43+ // step statuses from the pod before terminating them.
44+ func TestReconcileTimeoutWithEmptyStepStatus (t * testing.T ) {
45+ task := parse .MustParseV1Task (t , `
46+ metadata:
47+ name: test-task
48+ namespace: foo
49+ spec:
50+ steps:
51+ - name: step1
52+ image: busybox
53+ - name: step2
54+ image: busybox
55+ ` )
56+
57+ taskRun := parse .MustParseV1TaskRun (t , `
58+ metadata:
59+ name: test-taskrun-timeout-empty-steps
60+ namespace: foo
61+ spec:
62+ taskRef:
63+ name: test-task
64+ timeout: 1s
65+ status:
66+ conditions:
67+ - status: Unknown
68+ type: Succeeded
69+ startTime: "2021-12-31T23:59:58Z"
70+ podName: test-taskrun-timeout-empty-steps-pod
71+ ` )
72+ // In a real scenario, TaskSpec would have been set during pod creation in a previous reconcile
73+ taskRun .Status .TaskSpec = & task .Spec
74+
75+ // Create a pod with running step containers
76+ // This simulates a pod that exists but hasn't had its status synced to the TaskRun yet
77+ pod := & corev1.Pod {
78+ ObjectMeta : metav1.ObjectMeta {
79+ Namespace : "foo" ,
80+ Name : "test-taskrun-timeout-empty-steps-pod" ,
81+ },
82+ Status : corev1.PodStatus {
83+ Phase : corev1 .PodRunning ,
84+ ContainerStatuses : []corev1.ContainerStatus {
85+ {
86+ Name : "step-step1" ,
87+ State : corev1.ContainerState {
88+ Running : & corev1.ContainerStateRunning {
89+ StartedAt : metav1.Time {Time : testClock .Now ().Add (- 30 * time .Second )},
90+ },
91+ },
92+ },
93+ {
94+ Name : "step-step2" ,
95+ State : corev1.ContainerState {
96+ Waiting : & corev1.ContainerStateWaiting {
97+ Reason : "PodInitializing" ,
98+ },
99+ },
100+ },
101+ },
102+ },
103+ }
104+
105+ d := test.Data {
106+ TaskRuns : []* v1.TaskRun {taskRun },
107+ Tasks : []* v1.Task {task },
108+ Pods : []* corev1.Pod {pod },
109+ }
110+
111+ testAssets , cancel := getTaskRunController (t , d )
112+ defer cancel ()
113+ c := testAssets .Controller
114+ clients := testAssets .Clients
115+
116+ if err := c .Reconciler .Reconcile (testAssets .Ctx , getRunName (taskRun )); err != nil {
117+ t .Fatalf ("Unexpected error when reconciling timed out TaskRun: %v" , err )
118+ }
119+
120+ reconciledRun , err := clients .Pipeline .TektonV1 ().TaskRuns ("foo" ).Get (testAssets .Ctx , "test-taskrun-timeout-empty-steps" , metav1.GetOptions {})
121+ if err != nil {
122+ t .Fatalf ("Error getting reconciled TaskRun: %v" , err )
123+ }
124+
125+ // Verify the TaskRun is marked as timed out
126+ condition := reconciledRun .Status .GetCondition (apis .ConditionSucceeded )
127+ if condition == nil || condition .Status != corev1 .ConditionFalse {
128+ t .Errorf ("Expected TaskRun to be failed, got condition: %v" , condition )
129+ }
130+ if condition != nil && condition .Reason != v1 .TaskRunReasonTimedOut .String () {
131+ t .Errorf ("Expected TaskRun reason to be %s, got %s" , v1 .TaskRunReasonTimedOut .String (), condition .Reason )
132+ }
133+
134+ // Verify that step statuses are populated and terminated
135+ if len (reconciledRun .Status .Steps ) == 0 {
136+ t .Fatal ("Expected step statuses to be populated, but got empty Steps array" )
137+ }
138+
139+ if len (reconciledRun .Status .Steps ) != 2 {
140+ t .Fatalf ("Expected 2 steps, got %d" , len (reconciledRun .Status .Steps ))
141+ }
142+
143+ // Verify all steps are terminated with TimedOut reason
144+ for i , step := range reconciledRun .Status .Steps {
145+ if step .Terminated == nil {
146+ t .Errorf ("Step %d (%s) should have Terminated status, but it's nil" , i , step .Name )
147+ continue
148+ }
149+ if step .Terminated .Reason != v1 .TaskRunReasonTimedOut .String () {
150+ t .Errorf ("Step %d (%s) should have reason %s, got %s" ,
151+ i , step .Name , v1 .TaskRunReasonTimedOut .String (), step .Terminated .Reason )
152+ }
153+ if step .TerminationReason != v1 .TaskRunReasonTimedOut .String () {
154+ t .Errorf ("Step %d (%s) should have TerminationReason %s, got %s" ,
155+ i , step .Name , v1 .TaskRunReasonTimedOut .String (), step .TerminationReason )
156+ }
157+ }
158+ }
159+
40160func TestFailTaskRun_Timeout (t * testing.T ) {
41161 testCases := []struct {
42162 name string
0 commit comments