github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/pipeline/controller_test.go (about) 1 /* 2 Copyright 2019 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 main 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "reflect" 24 "strings" 25 "testing" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/sirupsen/logrus" 30 pipelinev1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" 31 32 corev1 "k8s.io/api/core/v1" 33 "k8s.io/apimachinery/pkg/api/equality" 34 apierrors "k8s.io/apimachinery/pkg/api/errors" 35 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 36 "k8s.io/apimachinery/pkg/util/diff" 37 "k8s.io/client-go/util/workqueue" 38 "knative.dev/pkg/apis" 39 duckv1 "knative.dev/pkg/apis/duck/v1" 40 prowjobv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 41 "sigs.k8s.io/prow/pkg/config" 42 "sigs.k8s.io/prow/pkg/kube" 43 "sigs.k8s.io/prow/pkg/pod-utils/decorate" 44 "sigs.k8s.io/prow/pkg/pod-utils/downwardapi" 45 ) 46 47 const ( 48 errorGetProwJob = "error-get-prowjob" 49 errorGetPipelineRun = "error-get-pipeline" 50 errorDeletePipelineRun = "error-delete-pipeline" 51 errorCreatePipelineRun = "error-create-pipeline" 52 errorUpdateProwJob = "error-update-prowjob" 53 pipelineID = "123" 54 ) 55 56 type fakeReconciler struct { 57 jobs map[string]prowjobv1.ProwJob 58 pipelines map[string]pipelinev1beta1.PipelineRun 59 nows metav1.Time 60 } 61 62 func (r *fakeReconciler) now() metav1.Time { 63 fmt.Println(r.nows) 64 return r.nows 65 } 66 67 const fakePJCtx = "prow-context" 68 const fakePJNS = "prow-job" 69 70 func (r *fakeReconciler) getProwJob(name string) (*prowjobv1.ProwJob, error) { 71 logrus.Debugf("getProwJob: name=%s", name) 72 if name == errorGetProwJob { 73 return nil, errors.New("injected get prowjob error") 74 } 75 k := toKey(fakePJCtx, fakePJNS, name) 76 pj, present := r.jobs[k] 77 if !present { 78 return nil, apierrors.NewNotFound(prowjobv1.Resource("ProwJob"), name) 79 } 80 return &pj, nil 81 } 82 83 func (r *fakeReconciler) patchProwJob(pj *prowjobv1.ProwJob, newpj *prowjobv1.ProwJob) (*prowjobv1.ProwJob, error) { 84 logrus.Debugf("patchProwJob: name=%s", pj.GetName()) 85 if pj.Name == errorUpdateProwJob { 86 return nil, errors.New("injected update prowjob error") 87 } 88 if pj == nil { 89 return nil, errors.New("nil prowjob") 90 } 91 k := toKey(fakePJCtx, fakePJNS, pj.Name) 92 if _, present := r.jobs[k]; !present { 93 return nil, apierrors.NewNotFound(prowjobv1.Resource("ProwJob"), pj.Name) 94 } 95 r.jobs[k] = *newpj 96 return newpj, nil 97 } 98 99 func (r *fakeReconciler) getPipelineRun(context, namespace, name string) (*pipelinev1beta1.PipelineRun, error) { 100 logrus.Debugf("getPipelineRun: ctx=%s, ns=%s, name=%s", context, namespace, name) 101 if namespace == errorGetPipelineRun { 102 return nil, errors.New("injected create pipeline error") 103 } 104 k := toKey(context, namespace, name) 105 p, present := r.pipelines[k] 106 if !present { 107 return nil, apierrors.NewNotFound(pipelinev1beta1.Resource("PipelineRun"), name) 108 } 109 return &p, nil 110 } 111 112 func (r *fakeReconciler) listProwJobs(namespace string) ([]*prowjobv1.ProwJob, error) { 113 logrus.Debugf("listProwJobs: namespace=%s", namespace) 114 pjs := []*prowjobv1.ProwJob{} 115 for _, v := range r.jobs { 116 if v.Namespace == namespace { 117 pjs = append(pjs, v.DeepCopy()) 118 } 119 } 120 return pjs, nil 121 } 122 123 func (r *fakeReconciler) deletePipelineRun(context, namespace, name string) error { 124 logrus.Debugf("deletePipelineRun: ctx=%s, ns=%s, name=%s", context, namespace, name) 125 if namespace == errorDeletePipelineRun { 126 return errors.New("injected create pipeline error") 127 } 128 k := toKey(context, namespace, name) 129 if _, present := r.pipelines[k]; !present { 130 return apierrors.NewNotFound(pipelinev1beta1.Resource("PipelineRun"), name) 131 } 132 delete(r.pipelines, k) 133 return nil 134 } 135 136 func (r *fakeReconciler) createPipelineRun(context, namespace string, p *pipelinev1beta1.PipelineRun) (*pipelinev1beta1.PipelineRun, error) { 137 logrus.Debugf("createPipelineRun: ctx=%s, ns=%s", context, namespace) 138 if p == nil { 139 return nil, errors.New("nil pipeline") 140 } 141 if namespace == errorCreatePipelineRun { 142 return nil, errors.New("injected create pipeline error") 143 } 144 k := toKey(context, namespace, p.Name) 145 if _, alreadyExists := r.pipelines[k]; alreadyExists { 146 return nil, apierrors.NewAlreadyExists(prowjobv1.Resource("ProwJob"), p.Name) 147 } 148 r.pipelines[k] = *p 149 return p, nil 150 } 151 152 func (r *fakeReconciler) pipelineID(pj prowjobv1.ProwJob) (string, string, error) { 153 return pipelineID, "", nil 154 } 155 156 func (r *fakeReconciler) cancelPipelineRun(context string, pr *pipelinev1beta1.PipelineRun) error { 157 pr.Spec.Status = pipelinev1beta1.PipelineRunSpecStatusCancelledRunFinally 158 return nil 159 } 160 161 type fakeLimiter struct { 162 workqueue.RateLimitingInterface 163 added string 164 } 165 166 func (fl *fakeLimiter) ShutDown() {} 167 func (fl *fakeLimiter) ShuttingDown() bool { 168 return false 169 } 170 func (fl *fakeLimiter) Get() (interface{}, bool) { 171 return "not implemented", true 172 } 173 func (fl *fakeLimiter) Done(interface{}) {} 174 func (fl *fakeLimiter) Forget(interface{}) {} 175 func (fl *fakeLimiter) AddRateLimited(a interface{}) { 176 fl.added = a.(string) 177 } 178 func (fl *fakeLimiter) Add(a interface{}) { 179 fl.added = a.(string) 180 } 181 func (fl *fakeLimiter) AddAfter(a interface{}, d time.Duration) { 182 fl.added = a.(string) 183 } 184 func (fl *fakeLimiter) Len() int { 185 return 0 186 } 187 func (fl *fakeLimiter) NumRequeues(item interface{}) int { 188 return 0 189 } 190 191 func TestEnqueueKey(t *testing.T) { 192 cases := []struct { 193 name string 194 context string 195 obj interface{} 196 expected string 197 }{ 198 { 199 name: "enqueue pipeline directly", 200 context: "hey", 201 obj: &pipelinev1beta1.PipelineRun{ 202 ObjectMeta: metav1.ObjectMeta{ 203 Namespace: "foo", 204 Name: "bar", 205 }, 206 }, 207 expected: toKey("hey", "foo", "bar"), 208 }, 209 { 210 name: "enqueue prowjob's spec namespace", 211 context: "rolo", 212 obj: &prowjobv1.ProwJob{ 213 ObjectMeta: metav1.ObjectMeta{ 214 Namespace: "default", 215 Name: "dude", 216 }, 217 Spec: prowjobv1.ProwJobSpec{ 218 Namespace: "tomassi", 219 }, 220 }, 221 expected: toKey("rolo", "tomassi", "dude"), 222 }, 223 { 224 name: "ignore random object", 225 context: "foo", 226 obj: "bar", 227 }, 228 } 229 230 for _, tc := range cases { 231 t.Run(tc.name, func(t *testing.T) { 232 var fl fakeLimiter 233 c := controller{ 234 workqueue: &fl, 235 } 236 c.enqueueKey(tc.context, tc.obj) 237 if !reflect.DeepEqual(fl.added, tc.expected) { 238 t.Errorf("%q != expected %q", fl.added, tc.expected) 239 } 240 }) 241 } 242 } 243 244 func TestReconcile(t *testing.T) { 245 duplicateAppendix := "-duplicate" 246 logrus.SetLevel(logrus.DebugLevel) 247 now := metav1.Now() 248 future := metav1.Time{Time: time.Now().Add(time.Minute)} 249 pipelineSpec := pipelinev1beta1.PipelineRunSpec{} 250 noJobChange := func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 251 return pj 252 } 253 noPipelineRunChange := func(_ prowjobv1.ProwJob, p pipelinev1beta1.PipelineRun) pipelinev1beta1.PipelineRun { 254 return p 255 } 256 cases := []struct { 257 name string 258 namespace string 259 context string 260 observedJob *prowjobv1.ProwJob 261 observedPipelineRun *pipelinev1beta1.PipelineRun 262 expectedJob func(prowjobv1.ProwJob, pipelinev1beta1.PipelineRun) prowjobv1.ProwJob 263 expectedPipelineRun func(prowjobv1.ProwJob, pipelinev1beta1.PipelineRun) pipelinev1beta1.PipelineRun 264 duplicateStartTime *metav1.Time 265 err bool 266 }{ 267 { 268 name: "new prow job creates pipeline", 269 observedJob: &prowjobv1.ProwJob{ 270 Spec: prowjobv1.ProwJobSpec{ 271 Agent: prowjobv1.TektonAgent, 272 PipelineRunSpec: &pipelineSpec, 273 }, 274 Status: prowjobv1.ProwJobStatus{ 275 BuildID: pipelineID, 276 }, 277 }, 278 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 279 pj.Status = prowjobv1.ProwJobStatus{ 280 StartTime: now, 281 PendingTime: &now, 282 State: prowjobv1.PendingState, 283 Description: descScheduling, 284 BuildID: pipelineID, 285 } 286 return pj 287 }, 288 expectedPipelineRun: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) pipelinev1beta1.PipelineRun { 289 pj.Spec.Type = prowjobv1.PeriodicJob 290 p, err := makePipelineRun(pj) 291 if err != nil { 292 panic(err) 293 } 294 return *p 295 }, 296 }, 297 { 298 name: "new prow job followed by a newer duplicate does not create a pipeline", 299 duplicateStartTime: &future, 300 observedJob: &prowjobv1.ProwJob{ 301 Status: prowjobv1.ProwJobStatus{ 302 State: prowjobv1.PendingState, 303 BuildID: pipelineID, 304 StartTime: now, 305 }, 306 Spec: prowjobv1.ProwJobSpec{ 307 Agent: prowjobv1.TektonAgent, 308 PipelineRunSpec: &pipelineSpec, 309 Job: "foobar", 310 }, 311 }, 312 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 313 if !strings.Contains(pj.Name, duplicateAppendix) { 314 pj.Status = prowjobv1.ProwJobStatus{ 315 StartTime: now, 316 State: prowjobv1.AbortedState, 317 Description: descAborted, 318 BuildID: pipelineID, 319 CompletionTime: &now, 320 } 321 return pj 322 } 323 pj.Status = prowjobv1.ProwJobStatus{ 324 StartTime: future, 325 State: prowjobv1.PendingState, 326 BuildID: pipelineID + duplicateAppendix, 327 } 328 return pj 329 }, 330 }, 331 { 332 name: "new prow job preceded by an older duplicate creates a pipeline", 333 duplicateStartTime: &now, 334 observedJob: &prowjobv1.ProwJob{ 335 Status: prowjobv1.ProwJobStatus{ 336 State: prowjobv1.PendingState, 337 BuildID: pipelineID, 338 StartTime: future, 339 }, 340 Spec: prowjobv1.ProwJobSpec{ 341 Agent: prowjobv1.TektonAgent, 342 PipelineRunSpec: &pipelineSpec, 343 Job: "foobar", 344 }, 345 }, 346 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 347 if !strings.Contains(pj.Name, duplicateAppendix) { 348 pj.Status = prowjobv1.ProwJobStatus{ 349 StartTime: future, 350 State: prowjobv1.PendingState, 351 Description: descScheduling, 352 BuildID: pipelineID, 353 } 354 return pj 355 } 356 pj.Status = prowjobv1.ProwJobStatus{ 357 StartTime: now, 358 State: prowjobv1.AbortedState, 359 BuildID: pipelineID + duplicateAppendix, 360 CompletionTime: &now, 361 Description: descAborted, 362 } 363 return pj 364 }, 365 expectedPipelineRun: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) pipelinev1beta1.PipelineRun { 366 pj.Spec.Type = prowjobv1.PeriodicJob 367 p, err := makePipelineRun(pj) 368 if err != nil { 369 panic(err) 370 } 371 return *p 372 }, 373 }, 374 { 375 name: "do not create pipeline run for failed prowjob", 376 observedJob: &prowjobv1.ProwJob{ 377 Spec: prowjobv1.ProwJobSpec{ 378 Agent: prowjobv1.TektonAgent, 379 PipelineRunSpec: &pipelineSpec, 380 }, 381 Status: prowjobv1.ProwJobStatus{ 382 State: prowjobv1.FailureState, 383 BuildID: pipelineID, 384 }, 385 }, 386 expectedJob: noJobChange, 387 }, 388 { 389 name: "do not create pipeline run for successful prowjob", 390 observedJob: &prowjobv1.ProwJob{ 391 Spec: prowjobv1.ProwJobSpec{ 392 Agent: prowjobv1.TektonAgent, 393 PipelineRunSpec: &pipelineSpec, 394 }, 395 Status: prowjobv1.ProwJobStatus{ 396 State: prowjobv1.SuccessState, 397 }, 398 }, 399 expectedJob: noJobChange, 400 }, 401 { 402 name: "do not create pipeline run for aborted prowjob", 403 observedJob: &prowjobv1.ProwJob{ 404 Spec: prowjobv1.ProwJobSpec{ 405 Agent: prowjobv1.TektonAgent, 406 PipelineRunSpec: &pipelineSpec, 407 }, 408 Status: prowjobv1.ProwJobStatus{ 409 State: prowjobv1.AbortedState, 410 BuildID: pipelineID, 411 }, 412 }, 413 expectedJob: noJobChange, 414 }, 415 { 416 name: "delete pipeline run after deleting prowjob", 417 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 418 pj := prowjobv1.ProwJob{} 419 pj.Spec.Type = prowjobv1.PeriodicJob 420 pj.Spec.PipelineRunSpec = &pipelinev1beta1.PipelineRunSpec{} 421 pj.Status.BuildID = pipelineID 422 p, err := makePipelineRun(pj) 423 if err != nil { 424 panic(err) 425 } 426 return p 427 }(), 428 }, 429 { 430 name: "do not delete deleted pipeline runs", 431 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 432 pj := prowjobv1.ProwJob{} 433 pj.Spec.Type = prowjobv1.PeriodicJob 434 pj.Spec.PipelineRunSpec = &pipelinev1beta1.PipelineRunSpec{} 435 pj.Status.BuildID = pipelineID 436 p, err := makePipelineRun(pj) 437 p.DeletionTimestamp = &now 438 if err != nil { 439 panic(err) 440 } 441 return p 442 }(), 443 expectedPipelineRun: noPipelineRunChange, 444 }, 445 { 446 name: "only delete pipeline runs created by controller", 447 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 448 pj := prowjobv1.ProwJob{} 449 pj.Spec.Type = prowjobv1.PeriodicJob 450 pj.Spec.PipelineRunSpec = &pipelinev1beta1.PipelineRunSpec{} 451 pj.Status.BuildID = pipelineID 452 p, err := makePipelineRun(pj) 453 if err != nil { 454 panic(err) 455 } 456 delete(p.Labels, kube.CreatedByProw) 457 return p 458 }(), 459 expectedPipelineRun: noPipelineRunChange, 460 }, 461 { 462 name: "delete prow pipeline runs in the wrong cluster", 463 context: "wrong-cluster", 464 observedJob: &prowjobv1.ProwJob{ 465 Spec: prowjobv1.ProwJobSpec{ 466 Agent: prowjobv1.TektonAgent, 467 Cluster: "target-cluster", 468 PipelineRunSpec: &pipelinev1beta1.PipelineRunSpec{ 469 ServiceAccountName: "robot", 470 }, 471 }, 472 Status: prowjobv1.ProwJobStatus{ 473 State: prowjobv1.PendingState, 474 StartTime: metav1.Now(), 475 Description: "fancy", 476 }, 477 }, 478 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 479 pj := prowjobv1.ProwJob{} 480 pj.Spec.Type = prowjobv1.PeriodicJob 481 pj.Spec.Agent = prowjobv1.TektonAgent 482 pj.Spec.PipelineRunSpec = &pipelineSpec 483 pj.Status.BuildID = pipelineID 484 p, err := makePipelineRun(pj) 485 if err != nil { 486 panic(err) 487 } 488 return p 489 }(), 490 expectedJob: noJobChange, 491 }, 492 { 493 name: "ignore random pipeline run in the wrong cluster", 494 context: "wrong-cluster", 495 observedJob: &prowjobv1.ProwJob{ 496 Spec: prowjobv1.ProwJobSpec{ 497 Agent: prowjobv1.TektonAgent, 498 Cluster: "target-cluster", 499 PipelineRunSpec: &pipelinev1beta1.PipelineRunSpec{ 500 ServiceAccountName: "robot", 501 }, 502 }, 503 Status: prowjobv1.ProwJobStatus{ 504 State: prowjobv1.PendingState, 505 StartTime: metav1.Now(), 506 Description: "fancy", 507 }, 508 }, 509 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 510 pj := prowjobv1.ProwJob{} 511 pj.Spec.Type = prowjobv1.PeriodicJob 512 pj.Spec.Agent = prowjobv1.TektonAgent 513 pj.Spec.PipelineRunSpec = &pipelineSpec 514 pj.Status.BuildID = pipelineID 515 p, err := makePipelineRun(pj) 516 if err != nil { 517 panic(err) 518 } 519 delete(p.Labels, kube.CreatedByProw) 520 return p 521 }(), 522 expectedJob: noJobChange, 523 expectedPipelineRun: noPipelineRunChange, 524 }, 525 { 526 name: "update job status if pipeline run resets", 527 observedJob: &prowjobv1.ProwJob{ 528 Spec: prowjobv1.ProwJobSpec{ 529 Agent: prowjobv1.TektonAgent, 530 PipelineRunSpec: &pipelinev1beta1.PipelineRunSpec{ 531 ServiceAccountName: "robot", 532 }, 533 }, 534 Status: prowjobv1.ProwJobStatus{ 535 State: prowjobv1.PendingState, 536 StartTime: metav1.Now(), 537 Description: "fancy", 538 }, 539 }, 540 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 541 pj := prowjobv1.ProwJob{} 542 pj.Spec.Type = prowjobv1.PeriodicJob 543 pj.Spec.Agent = prowjobv1.TektonAgent 544 pj.Spec.PipelineRunSpec = &pipelinev1beta1.PipelineRunSpec{ 545 ServiceAccountName: "robot", 546 } 547 pj.Status.BuildID = pipelineID 548 p, err := makePipelineRun(pj) 549 if err != nil { 550 panic(err) 551 } 552 return p 553 }(), 554 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 555 pj.Status.State = prowjobv1.PendingState 556 pj.Status.Description = descScheduling 557 return pj 558 }, 559 expectedPipelineRun: noPipelineRunChange, 560 }, 561 { 562 name: "prowjob goes pending when pipeline run starts", 563 observedJob: &prowjobv1.ProwJob{ 564 Spec: prowjobv1.ProwJobSpec{ 565 Agent: prowjobv1.TektonAgent, 566 PipelineRunSpec: &pipelineSpec, 567 }, 568 Status: prowjobv1.ProwJobStatus{ 569 State: prowjobv1.PendingState, 570 Description: "fancy", 571 }, 572 }, 573 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 574 pj := prowjobv1.ProwJob{} 575 pj.Spec.Type = prowjobv1.PeriodicJob 576 pj.Spec.Agent = prowjobv1.TektonAgent 577 pj.Spec.PipelineRunSpec = &pipelineSpec 578 pj.Status.BuildID = pipelineID 579 p, err := makePipelineRun(pj) 580 if err != nil { 581 panic(err) 582 } 583 p.Status.SetCondition(&apis.Condition{ 584 Type: apis.ConditionReady, 585 Message: "hello", 586 }) 587 return p 588 }(), 589 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 590 pj.Status = prowjobv1.ProwJobStatus{ 591 StartTime: now, 592 State: prowjobv1.PendingState, 593 Description: "scheduling", 594 } 595 return pj 596 }, 597 expectedPipelineRun: noPipelineRunChange, 598 }, 599 { 600 name: "prowjob succeeds when run pipeline succeeds", 601 observedJob: &prowjobv1.ProwJob{ 602 Spec: prowjobv1.ProwJobSpec{ 603 Agent: prowjobv1.TektonAgent, 604 PipelineRunSpec: &pipelineSpec, 605 }, 606 Status: prowjobv1.ProwJobStatus{ 607 State: prowjobv1.PendingState, 608 Description: "fancy", 609 }, 610 }, 611 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 612 pj := prowjobv1.ProwJob{} 613 pj.Spec.Type = prowjobv1.PeriodicJob 614 pj.Spec.Agent = prowjobv1.TektonAgent 615 pj.Spec.PipelineRunSpec = &pipelineSpec 616 pj.Status.BuildID = pipelineID 617 p, err := makePipelineRun(pj) 618 if err != nil { 619 panic(err) 620 } 621 p.Status.SetCondition(&apis.Condition{ 622 Type: apis.ConditionSucceeded, 623 Status: corev1.ConditionTrue, 624 Message: "hello", 625 }) 626 return p 627 }(), 628 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 629 pj.Status = prowjobv1.ProwJobStatus{ 630 StartTime: now, 631 CompletionTime: &now, 632 State: prowjobv1.SuccessState, 633 Description: "hello", 634 } 635 return pj 636 }, 637 expectedPipelineRun: noPipelineRunChange, 638 }, 639 { 640 name: "prowjob fails when pipeline run fails", 641 observedJob: &prowjobv1.ProwJob{ 642 Spec: prowjobv1.ProwJobSpec{ 643 Agent: prowjobv1.TektonAgent, 644 PipelineRunSpec: &pipelineSpec, 645 }, 646 Status: prowjobv1.ProwJobStatus{ 647 State: prowjobv1.PendingState, 648 Description: "fancy", 649 }, 650 }, 651 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 652 pj := prowjobv1.ProwJob{} 653 pj.Spec.Type = prowjobv1.PeriodicJob 654 pj.Spec.Agent = prowjobv1.TektonAgent 655 pj.Spec.PipelineRunSpec = &pipelineSpec 656 pj.Status.BuildID = pipelineID 657 p, err := makePipelineRun(pj) 658 if err != nil { 659 panic(err) 660 } 661 p.Status.SetCondition(&apis.Condition{ 662 Type: apis.ConditionSucceeded, 663 Status: corev1.ConditionFalse, 664 Message: "hello", 665 }) 666 return p 667 }(), 668 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 669 pj.Status = prowjobv1.ProwJobStatus{ 670 StartTime: now, 671 CompletionTime: &now, 672 State: prowjobv1.FailureState, 673 Description: "hello", 674 } 675 return pj 676 }, 677 expectedPipelineRun: noPipelineRunChange, 678 }, 679 { 680 name: "error when we cannot get prowjob", 681 namespace: errorGetProwJob, 682 err: true, 683 observedJob: &prowjobv1.ProwJob{ 684 Spec: prowjobv1.ProwJobSpec{ 685 Agent: prowjobv1.TektonAgent, 686 PipelineRunSpec: &pipelineSpec, 687 }, 688 Status: prowjobv1.ProwJobStatus{ 689 State: prowjobv1.PendingState, 690 Description: "fancy", 691 }, 692 }, 693 }, 694 { 695 name: "error when we cannot get pipeline run", 696 namespace: errorGetPipelineRun, 697 err: true, 698 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 699 pj := prowjobv1.ProwJob{} 700 pj.Spec.Type = prowjobv1.PeriodicJob 701 pj.Spec.Agent = prowjobv1.TektonAgent 702 pj.Spec.PipelineRunSpec = &pipelineSpec 703 pj.Status.BuildID = pipelineID 704 p, err := makePipelineRun(pj) 705 if err != nil { 706 panic(err) 707 } 708 p.Status.SetCondition(&apis.Condition{ 709 Type: apis.ConditionSucceeded, 710 Status: corev1.ConditionTrue, 711 Message: "hello", 712 }) 713 return p 714 }(), 715 }, 716 { 717 name: "error when we cannot delete pipeline run", 718 namespace: errorDeletePipelineRun, 719 err: true, 720 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 721 pj := prowjobv1.ProwJob{} 722 pj.Spec.Type = prowjobv1.PeriodicJob 723 pj.Spec.PipelineRunSpec = &pipelinev1beta1.PipelineRunSpec{} 724 pj.Status.BuildID = pipelineID 725 p, err := makePipelineRun(pj) 726 if err != nil { 727 panic(err) 728 } 729 return p 730 }(), 731 }, 732 { 733 name: "set prow job in error state when we cannot create pipeline run", 734 namespace: errorCreatePipelineRun, 735 err: false, 736 observedJob: &prowjobv1.ProwJob{ 737 Spec: prowjobv1.ProwJobSpec{ 738 Agent: prowjobv1.TektonAgent, 739 PipelineRunSpec: &pipelineSpec, 740 }, 741 }, 742 expectedJob: func(pj prowjobv1.ProwJob, _ pipelinev1beta1.PipelineRun) prowjobv1.ProwJob { 743 pj.Status = prowjobv1.ProwJobStatus{ 744 BuildID: pipelineID, 745 StartTime: now, 746 CompletionTime: &now, 747 State: prowjobv1.ErrorState, 748 Description: "start pipeline: injected create pipeline error", 749 } 750 return pj 751 }, 752 }, 753 { 754 name: "error when pipelinerunspec is nil", 755 err: true, 756 observedJob: &prowjobv1.ProwJob{ 757 Spec: prowjobv1.ProwJobSpec{ 758 Agent: prowjobv1.TektonAgent, 759 PipelineRunSpec: nil, 760 }, 761 Status: prowjobv1.ProwJobStatus{ 762 State: prowjobv1.TriggeredState, 763 }, 764 }, 765 }, 766 { 767 name: "error when we cannot update prowjob", 768 namespace: errorUpdateProwJob, 769 err: true, 770 observedJob: &prowjobv1.ProwJob{ 771 Spec: prowjobv1.ProwJobSpec{ 772 Agent: prowjobv1.TektonAgent, 773 PipelineRunSpec: &pipelineSpec, 774 }, 775 Status: prowjobv1.ProwJobStatus{ 776 State: prowjobv1.PendingState, 777 Description: "fancy", 778 }, 779 }, 780 observedPipelineRun: func() *pipelinev1beta1.PipelineRun { 781 pj := prowjobv1.ProwJob{} 782 pj.Spec.Type = prowjobv1.PeriodicJob 783 pj.Spec.Agent = prowjobv1.TektonAgent 784 pj.Spec.PipelineRunSpec = &pipelineSpec 785 pj.Status.BuildID = pipelineID 786 p, err := makePipelineRun(pj) 787 if err != nil { 788 panic(err) 789 } 790 p.Status.SetCondition(&apis.Condition{ 791 Type: apis.ConditionSucceeded, 792 Status: corev1.ConditionTrue, 793 Message: "hello", 794 }) 795 return p 796 }(), 797 }} 798 799 for _, tc := range cases { 800 t.Run(tc.name, func(t *testing.T) { 801 name := "the-object-name" 802 // prowjobs all live in the same ns, so use name for injecting errors 803 if tc.namespace == errorGetProwJob { 804 name = errorGetProwJob 805 } else if tc.namespace == errorUpdateProwJob { 806 name = errorUpdateProwJob 807 } 808 if tc.context == "" { 809 tc.context = kube.DefaultClusterAlias 810 } 811 r := &fakeReconciler{ 812 jobs: map[string]prowjobv1.ProwJob{}, 813 pipelines: map[string]pipelinev1beta1.PipelineRun{}, 814 nows: now, 815 } 816 817 jk := toKey(fakePJCtx, fakePJNS, name) 818 jkDuplicate := toKey(fakePJCtx, fakePJNS, name+duplicateAppendix) 819 if j := tc.observedJob; j != nil { 820 j.Name = name 821 j.Spec.Type = prowjobv1.PeriodicJob 822 if tc.duplicateStartTime != nil { 823 duplicate := j.DeepCopy() 824 duplicate.Name = name + duplicateAppendix 825 duplicate.Status.StartTime = *tc.duplicateStartTime 826 duplicate.Status.BuildID = j.Status.BuildID + duplicateAppendix 827 r.jobs[jkDuplicate] = *duplicate 828 } 829 r.jobs[jk] = *j 830 } 831 pk := toKey(tc.context, tc.namespace, name) 832 if p := tc.observedPipelineRun; p != nil { 833 p.Name = name 834 p.Labels[kube.ProwJobIDLabel] = name 835 r.pipelines[pk] = *p 836 } 837 838 expectedJobs := map[string]prowjobv1.ProwJob{} 839 if j := tc.expectedJob; j != nil { 840 expectedJobs[jk] = j(r.jobs[jk], r.pipelines[pk]) 841 if tc.duplicateStartTime != nil { 842 // use empty pipeline run as we don't care about the duplicate 843 expectedJobs[jkDuplicate] = j(r.jobs[jkDuplicate], pipelinev1beta1.PipelineRun{}) 844 } 845 } 846 expectedPipelineRuns := map[string]pipelinev1beta1.PipelineRun{} 847 if p := tc.expectedPipelineRun; p != nil { 848 expectedPipelineRuns[pk] = p(r.jobs[jk], r.pipelines[pk]) 849 } 850 851 tk := toKey(tc.context, tc.namespace, name) 852 err := reconcile(r, tk) 853 switch { 854 case err != nil: 855 if !tc.err { 856 t.Errorf("unexpected error: %v", err) 857 } 858 case tc.err: 859 t.Error("failed to receive expected error") 860 case !equality.Semantic.DeepEqual(r.jobs, expectedJobs): 861 t.Errorf("prowjobs do not match:\n%s", diff.ObjectReflectDiff(expectedJobs, r.jobs)) 862 case !equality.Semantic.DeepEqual(r.pipelines, expectedPipelineRuns): 863 t.Errorf("pipelineruns do not match:\n%s", diff.ObjectReflectDiff(expectedPipelineRuns, r.pipelines)) 864 } 865 }) 866 } 867 } 868 869 func TestPipelineMeta(t *testing.T) { 870 cases := []struct { 871 name string 872 pj prowjobv1.ProwJob 873 expected func(prowjobv1.ProwJob, *metav1.ObjectMeta) 874 }{ 875 { 876 name: "Use pj.Spec.Namespace for pipeline namespace", 877 pj: prowjobv1.ProwJob{ 878 ObjectMeta: metav1.ObjectMeta{ 879 Name: "whatever", 880 Namespace: "wrong", 881 }, 882 Spec: prowjobv1.ProwJobSpec{ 883 Namespace: "correct", 884 }, 885 }, 886 expected: func(pj prowjobv1.ProwJob, meta *metav1.ObjectMeta) { 887 meta.Name = pj.Name 888 meta.Namespace = pj.Spec.Namespace 889 meta.Labels, meta.Annotations = decorate.LabelsAndAnnotationsForJob(pj) 890 }, 891 }, 892 } 893 894 for _, tc := range cases { 895 t.Run(tc.name, func(t *testing.T) { 896 var expected metav1.ObjectMeta 897 tc.expected(tc.pj, &expected) 898 actual := pipelineMeta(tc.pj.Name, tc.pj) 899 if !equality.Semantic.DeepEqual(actual, expected) { 900 t.Errorf("pipeline meta does not match:\n%s", diff.ObjectReflectDiff(expected, actual)) 901 } 902 }) 903 } 904 } 905 906 func TestMakeResourcesBeta1(t *testing.T) { 907 cases := []struct { 908 name string 909 job func(prowjobv1.ProwJob) prowjobv1.ProwJob 910 pipelineRun func(pipelinev1beta1.PipelineRun) pipelinev1beta1.PipelineRun 911 err bool 912 }{ 913 { 914 name: "reject empty prow job", 915 job: func(_ prowjobv1.ProwJob) prowjobv1.ProwJob { return prowjobv1.ProwJob{} }, 916 err: true, 917 }, 918 { 919 name: "return valid pipeline with valid prowjob", 920 }, 921 { 922 name: "configure implicit git repository", 923 job: func(pj prowjobv1.ProwJob) prowjobv1.ProwJob { 924 pj.Spec.Type = prowjobv1.PresubmitJob 925 pj.Spec.Refs = &prowjobv1.Refs{ 926 CloneURI: "https://source.host/test/test.git", 927 BaseRef: "feature-branch", 928 Pulls: []prowjobv1.Pull{{Number: 1}}, 929 } 930 pj.Spec.TektonPipelineRunSpec.V1Beta1.PipelineSpec = &pipelinev1beta1.PipelineSpec{ 931 Tasks: []pipelinev1beta1.PipelineTask{{ 932 Name: "implicit git resource", 933 TaskRef: &pipelinev1beta1.TaskRef{Name: config.ProwImplicitGitResource}, 934 }}, 935 } 936 937 return pj 938 }, 939 pipelineRun: func(pr pipelinev1beta1.PipelineRun) pipelinev1beta1.PipelineRun { 940 pr.Spec.Params[4].Value = pipelinev1beta1.ParamValue{ 941 Type: pipelinev1beta1.ParamTypeString, 942 StringVal: string(prowjobv1.PresubmitJob), 943 } 944 pr.Spec.Params = append(pr.Spec.Params, 945 pipelinev1beta1.Param{Name: "PULL_BASE_REF", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: "feature-branch"}}, 946 pipelinev1beta1.Param{Name: "PULL_BASE_SHA", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString}}, 947 pipelinev1beta1.Param{Name: "PULL_HEAD_REF", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString}}, 948 pipelinev1beta1.Param{Name: "PULL_NUMBER", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: "1"}}, 949 pipelinev1beta1.Param{Name: "PULL_PULL_SHA", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString}}, 950 pipelinev1beta1.Param{Name: "PULL_REFS", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: "feature-branch,1:"}}, 951 pipelinev1beta1.Param{Name: "PULL_TITLE", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString}}, 952 pipelinev1beta1.Param{Name: "REPO_NAME", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString}}, 953 pipelinev1beta1.Param{Name: "REPO_OWNER", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString}}, 954 ) 955 pr.Spec.PipelineSpec.Tasks = []pipelinev1beta1.PipelineTask{ 956 { 957 TaskRef: &pipelinev1beta1.TaskRef{Name: "git-clone"}, 958 Params: []pipelinev1beta1.Param{ 959 {Name: "url", Value: pipelinev1beta1.ParamValue{StringVal: "https://source.host/test/test.git"}}, 960 {Name: "revision", Value: pipelinev1beta1.ParamValue{StringVal: "pull/1/head"}}, 961 }, 962 }, 963 } 964 return pr 965 }, 966 }, 967 { 968 name: "configure sources when extra refs are configured", 969 job: func(pj prowjobv1.ProwJob) prowjobv1.ProwJob { 970 pj.Spec.ExtraRefs = []prowjobv1.Refs{{Org: "org0"}, {Org: "org1"}} 971 pj.Spec.TektonPipelineRunSpec.V1Beta1.PipelineSpec = &pipelinev1beta1.PipelineSpec{ 972 Tasks: []pipelinev1beta1.PipelineTask{ 973 {Name: "git resource A", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_EXTRA_GIT_REF_0"}}, 974 {Name: "git resource B", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_EXTRA_GIT_REF_1"}}, 975 }, 976 } 977 return pj 978 }, 979 pipelineRun: func(pr pipelinev1beta1.PipelineRun) pipelinev1beta1.PipelineRun { 980 pr.Spec.PipelineSpec.Tasks = []pipelinev1beta1.PipelineTask{ 981 { 982 TaskRef: &pipelinev1beta1.TaskRef{Name: "git-clone"}, 983 Params: []pipelinev1beta1.Param{ 984 {Name: "url", Value: pipelinev1beta1.ParamValue{StringVal: "https://github.com/org0/.git"}}, 985 {Name: "revision"}, 986 }, 987 }, 988 { 989 TaskRef: &pipelinev1beta1.TaskRef{Name: "git-clone"}, 990 Params: []pipelinev1beta1.Param{ 991 {Name: "url", Value: pipelinev1beta1.ParamValue{StringVal: "https://github.com/org1/.git"}}, 992 {Name: "revision"}, 993 }, 994 }, 995 } 996 return pr 997 }, 998 }, 999 { 1000 name: "do not override unrelated git resources", 1001 job: func(pj prowjobv1.ProwJob) prowjobv1.ProwJob { 1002 pj.Spec.TektonPipelineRunSpec.V1Beta1.PipelineSpec = &pipelinev1beta1.PipelineSpec{ 1003 Tasks: []pipelinev1beta1.PipelineTask{ 1004 {Name: "git resource A", TaskRef: &pipelinev1beta1.TaskRef{Name: "PROW_EXTRA_GIT_REF_LOL_JK"}}, 1005 {Name: "git resource B", TaskRef: &pipelinev1beta1.TaskRef{Name: "some-other-ref"}}, 1006 }, 1007 } 1008 return pj 1009 }, 1010 }, 1011 } 1012 1013 for _, tc := range cases { 1014 t.Run(tc.name, func(t *testing.T) { 1015 const randomPipelineRunID = "so-many-pipelines" 1016 pj := prowjobv1.ProwJob{} 1017 pj.Name = "world" 1018 pj.Namespace = "hello" 1019 pj.Spec.Type = prowjobv1.PeriodicJob 1020 pj.Spec.Job = "ci-job" 1021 pj.Spec.TektonPipelineRunSpec = &prowjobv1.TektonPipelineRunSpec{ 1022 V1Beta1: &pipelinev1beta1.PipelineRunSpec{}, 1023 } 1024 pj.Status.BuildID = randomPipelineRunID 1025 1026 if tc.job != nil { 1027 pj = tc.job(pj) 1028 } 1029 1030 actualRun, err := makePipelineRun(pj) 1031 if err != nil { 1032 if !tc.err { 1033 t.Errorf("unexpected error: %v", err) 1034 } 1035 return 1036 } else if tc.err { 1037 t.Error("failed to receive expected error") 1038 } 1039 1040 jobSpecRaw, err := json.Marshal(downwardapi.NewJobSpec(pj.Spec, randomPipelineRunID, pj.Name)) 1041 if err != nil { 1042 t.Errorf("failed to marshal job spec: %v", err) 1043 } 1044 pipelineRunSpec, err := pj.Spec.GetPipelineRunSpec() 1045 if err != nil { 1046 t.Errorf("failed to get pipeline run spec: %v", err) 1047 } 1048 expectedRun := pipelinev1beta1.PipelineRun{ 1049 ObjectMeta: pipelineMeta(pj.Name, pj), 1050 Spec: *pipelineRunSpec, 1051 } 1052 expectedRun.Spec.Params = []pipelinev1beta1.Param{ 1053 {Name: "BUILD_ID", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: randomPipelineRunID}}, 1054 {Name: "CI", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: "true"}}, 1055 {Name: "JOB_NAME", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: pj.Spec.Job}}, 1056 {Name: "JOB_SPEC", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: string(jobSpecRaw)}}, 1057 {Name: "JOB_TYPE", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: string(prowjobv1.PeriodicJob)}}, 1058 {Name: "PROW_JOB_ID", Value: pipelinev1beta1.ParamValue{Type: pipelinev1beta1.ParamTypeString, StringVal: pj.Name}}, 1059 } 1060 if tc.pipelineRun != nil { 1061 expectedRun = tc.pipelineRun(expectedRun) 1062 } 1063 1064 if diff := cmp.Diff(actualRun, &expectedRun); diff != "" { 1065 t.Error(diff) 1066 } 1067 }) 1068 } 1069 } 1070 1071 func TestDescription(t *testing.T) { 1072 cases := []struct { 1073 name string 1074 message string 1075 reason string 1076 fallback string 1077 expected string 1078 }{ 1079 { 1080 name: "prefer message over reason or fallback", 1081 message: "hello", 1082 reason: "world", 1083 fallback: "doh", 1084 expected: "hello", 1085 }, 1086 { 1087 name: "prefer reason over fallback", 1088 reason: "world", 1089 fallback: "other", 1090 expected: "world", 1091 }, 1092 { 1093 name: "use fallback if nothing else set", 1094 fallback: "fancy", 1095 expected: "fancy", 1096 }, 1097 } 1098 1099 for _, tc := range cases { 1100 bc := apis.Condition{ 1101 Message: tc.message, 1102 Reason: tc.reason, 1103 } 1104 if actual := description(bc, tc.fallback); actual != tc.expected { 1105 t.Errorf("%s: actual %q != expected %q", tc.name, actual, tc.expected) 1106 } 1107 } 1108 } 1109 1110 func TestProwJobStatus(t *testing.T) { 1111 now := metav1.Now() 1112 later := metav1.NewTime(now.Time.Add(1 * time.Hour)) 1113 cases := []struct { 1114 name string 1115 input pipelinev1beta1.PipelineRunStatus 1116 state prowjobv1.ProwJobState 1117 desc string 1118 fallback string 1119 }{ 1120 { 1121 name: "empty conditions returns pending/scheduling", 1122 state: prowjobv1.PendingState, 1123 desc: descScheduling, 1124 }, 1125 { 1126 name: "truly succeeded state returns success", 1127 input: pipelinev1beta1.PipelineRunStatus{ 1128 Status: duckv1.Status{ 1129 Conditions: []apis.Condition{ 1130 { 1131 Type: apis.ConditionSucceeded, 1132 Status: corev1.ConditionTrue, 1133 Message: "fancy", 1134 }, 1135 }, 1136 }, 1137 }, 1138 state: prowjobv1.SuccessState, 1139 desc: "fancy", 1140 fallback: descSucceeded, 1141 }, 1142 { 1143 name: "falsely succeeded state returns failure", 1144 input: pipelinev1beta1.PipelineRunStatus{ 1145 Status: duckv1.Status{ 1146 Conditions: []apis.Condition{ 1147 { 1148 Type: apis.ConditionSucceeded, 1149 Status: corev1.ConditionFalse, 1150 Message: "weird", 1151 }, 1152 }, 1153 }, 1154 }, 1155 state: prowjobv1.FailureState, 1156 desc: "weird", 1157 fallback: descFailed, 1158 }, 1159 { 1160 name: "unstarted job returns pending/initializing", 1161 input: pipelinev1beta1.PipelineRunStatus{ 1162 Status: duckv1.Status{ 1163 Conditions: []apis.Condition{ 1164 { 1165 Type: apis.ConditionSucceeded, 1166 Status: corev1.ConditionUnknown, 1167 Message: "hola", 1168 }, 1169 }, 1170 }, 1171 }, 1172 state: prowjobv1.PendingState, 1173 desc: "hola", 1174 fallback: descInitializing, 1175 }, 1176 { 1177 name: "unfinished job returns running", 1178 input: pipelinev1beta1.PipelineRunStatus{ 1179 PipelineRunStatusFields: pipelinev1beta1.PipelineRunStatusFields{ 1180 StartTime: now.DeepCopy(), 1181 }, 1182 Status: duckv1.Status{ 1183 Conditions: []apis.Condition{ 1184 { 1185 Type: apis.ConditionSucceeded, 1186 Status: corev1.ConditionUnknown, 1187 Message: "hola", 1188 }, 1189 }, 1190 }, 1191 }, 1192 state: prowjobv1.PendingState, 1193 desc: "hola", 1194 fallback: descRunning, 1195 }, 1196 { 1197 name: "pipelines with unknown success status are still running", 1198 input: pipelinev1beta1.PipelineRunStatus{ 1199 PipelineRunStatusFields: pipelinev1beta1.PipelineRunStatusFields{ 1200 StartTime: now.DeepCopy(), 1201 CompletionTime: later.DeepCopy(), 1202 }, 1203 Status: duckv1.Status{ 1204 Conditions: []apis.Condition{ 1205 { 1206 Type: apis.ConditionSucceeded, 1207 Status: corev1.ConditionUnknown, 1208 Message: "hola", 1209 }, 1210 }, 1211 }, 1212 }, 1213 state: prowjobv1.PendingState, 1214 desc: "hola", 1215 fallback: descRunning, 1216 }, 1217 { 1218 name: "completed pipelines without a succeeded condition end in error", 1219 input: pipelinev1beta1.PipelineRunStatus{ 1220 PipelineRunStatusFields: pipelinev1beta1.PipelineRunStatusFields{ 1221 StartTime: now.DeepCopy(), 1222 CompletionTime: later.DeepCopy(), 1223 }, 1224 }, 1225 state: prowjobv1.ErrorState, 1226 desc: descMissingCondition, 1227 }, 1228 } 1229 1230 for _, tc := range cases { 1231 if len(tc.fallback) > 0 { 1232 tc.desc = tc.fallback 1233 tc.fallback = "" 1234 tc.name += " [fallback]" 1235 cond := tc.input.Conditions[0] 1236 cond.Message = "" 1237 tc.input.Conditions = []apis.Condition{cond} 1238 cases = append(cases, tc) 1239 } 1240 } 1241 1242 for _, tc := range cases { 1243 t.Run(tc.name, func(t *testing.T) { 1244 state, desc := prowJobStatus(tc.input) 1245 if state != tc.state { 1246 t.Errorf("state %q != expected %q", state, tc.state) 1247 } 1248 if desc != tc.desc { 1249 t.Errorf("description %q != expected %q", desc, tc.desc) 1250 } 1251 }) 1252 } 1253 }