sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/statusreconciler/migrator/migrator_test.go (about) 1 /* 2 Copyright 2017 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 migrator 18 19 import ( 20 "errors" 21 "fmt" 22 "testing" 23 24 "sigs.k8s.io/prow/pkg/github" 25 ) 26 27 type modeTest struct { 28 name string 29 start []github.Status 30 expectedDiffs []github.Status 31 } 32 33 // compareDiffs checks if a list of status updates matches an expected list of status updates. 34 func compareDiffs(diffs []github.Status, expectedDiffs []github.Status) error { 35 if len(diffs) != len(expectedDiffs) { 36 return fmt.Errorf("failed because the returned diff had %d changes instead of %d", len(diffs), len(expectedDiffs)) 37 } 38 for _, diff := range diffs { 39 if diff.Context == "" { 40 return fmt.Errorf("failed because the returned diff contained a Status with an empty Context field") 41 } 42 if diff.Description == "" { 43 return fmt.Errorf("failed because the returned diff contained a Status with an empty Description field") 44 } 45 if diff.State == "" { 46 return fmt.Errorf("failed because the returned diff contained a Status with an empty State field") 47 } 48 var match github.Status 49 var found bool 50 for _, expected := range expectedDiffs { 51 if expected.Context == diff.Context { 52 match = expected 53 found = true 54 break 55 } 56 } 57 if !found { 58 return fmt.Errorf("failed because the returned diff contained an unexpected change to context '%s'", diff.Context) 59 } 60 // Found a matching context. Make sure that fields are equal. 61 if match.Description != diff.Description { 62 return fmt.Errorf("failed because the returned diff for context '%s' had Description '%s' instead of '%s'", diff.Context, diff.Description, match.Description) 63 } 64 if match.State != diff.State { 65 return fmt.Errorf("failed because the returned diff for context '%s' had State '%s' instead of '%s'", diff.Context, diff.State, match.State) 66 } 67 68 if match.TargetURL == "" { 69 if diff.TargetURL != "" { 70 return fmt.Errorf("failed because the returned diff for context '%s' had a non-empty TargetURL", diff.Context) 71 } 72 } else if diff.TargetURL == "" { 73 return fmt.Errorf("failed because the returned diff for context '%s' had an empty TargetURL", diff.Context) 74 } else if match.TargetURL != diff.TargetURL { 75 return fmt.Errorf("failed because the returned diff for context '%s' had TargetURL '%s' instead of '%s'", diff.Context, diff.TargetURL, match.TargetURL) 76 } 77 } 78 return nil 79 } 80 81 func TestMoveMode(t *testing.T) { 82 contextA := "context A" 83 contextB := "context B" 84 desc := "Context retired. Status moved to \"context B\"." 85 86 tests := []*modeTest{ 87 { 88 name: "simple", 89 start: []github.Status{ 90 makeStatus(contextA, "failure", "description 1", "url 1"), 91 }, 92 expectedDiffs: []github.Status{ 93 makeStatus(contextA, "success", desc, ""), 94 makeStatus(contextB, "failure", "description 1", "url 1"), 95 }, 96 }, 97 { 98 name: "unrelated contexts", 99 start: []github.Status{ 100 makeStatus("also not related", "error", "description 4", "url 4"), 101 makeStatus(contextA, "failure", "description 1", "url 1"), 102 makeStatus("unrelated context", "success", "description 2", "url 2"), 103 }, 104 expectedDiffs: []github.Status{ 105 makeStatus(contextA, "success", desc, ""), 106 makeStatus(contextB, "failure", "description 1", "url 1"), 107 }, 108 }, 109 { 110 name: "unrelated contexts; missing context A", 111 start: []github.Status{ 112 makeStatus("also not related", "error", "description 4", "url 4"), 113 makeStatus("unrelated context", "success", "description 2", "url 2"), 114 }, 115 expectedDiffs: []github.Status{}, 116 }, 117 { 118 name: "unrelated contexts; already have context A and B", 119 start: []github.Status{ 120 makeStatus("also not related", "error", "description 4", "url 4"), 121 makeStatus(contextA, "failure", "description 1", "url 1"), 122 makeStatus("unrelated context", "success", "description 2", "url 2"), 123 makeStatus(contextB, "failure", "description 1", "url 1"), 124 }, 125 expectedDiffs: []github.Status{}, 126 }, 127 { 128 name: "unrelated contexts; already have context B; no context A", 129 start: []github.Status{ 130 makeStatus("also not related", "error", "description 4", "url 4"), 131 makeStatus("unrelated context", "success", "description 2", "url 2"), 132 makeStatus(contextB, "failure", "description 1", "url 1"), 133 }, 134 expectedDiffs: []github.Status{}, 135 }, 136 { 137 name: "no contexts", 138 start: []github.Status{}, 139 expectedDiffs: []github.Status{}, 140 }, 141 } 142 143 m := *MoveMode(contextA, contextB, "") 144 for _, test := range tests { 145 diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start}) 146 if err := compareDiffs(diff, test.expectedDiffs); err != nil { 147 t.Errorf("MoveMode test '%s' %v\n", test.name, err) 148 } 149 } 150 } 151 152 func TestCopyMode(t *testing.T) { 153 contextA := "context A" 154 contextB := "context B" 155 156 tests := []*modeTest{ 157 { 158 name: "simple", 159 start: []github.Status{ 160 makeStatus(contextA, "failure", "description 1", "url 1"), 161 }, 162 expectedDiffs: []github.Status{ 163 makeStatus(contextB, "failure", "description 1", "url 1"), 164 }, 165 }, 166 { 167 name: "unrelated contexts", 168 start: []github.Status{ 169 makeStatus("unrelated context", "success", "description 2", "url 2"), 170 makeStatus(contextA, "failure", "description 1", "url 1"), 171 makeStatus("also not related", "error", "description 4", "url 4"), 172 }, 173 expectedDiffs: []github.Status{ 174 makeStatus(contextB, "failure", "description 1", "url 1"), 175 }, 176 }, 177 { 178 name: "already have context B", 179 start: []github.Status{ 180 makeStatus(contextA, "failure", "description 1", "url 1"), 181 makeStatus(contextB, "failure", "description 1", "url 1"), 182 }, 183 expectedDiffs: []github.Status{}, 184 }, 185 { 186 name: "already have updated context B", 187 start: []github.Status{ 188 makeStatus(contextA, "failure", "description 1", "url 1"), 189 makeStatus(contextB, "success", "description 2", "url 2"), 190 }, 191 expectedDiffs: []github.Status{}, 192 }, 193 { 194 name: "unrelated contexts already have updated context B", 195 start: []github.Status{ 196 makeStatus("unrelated context", "success", "description 2", "url 2"), 197 makeStatus(contextA, "failure", "description 1", "url 1"), 198 makeStatus("also not related", "error", "description 4", "url 4"), 199 makeStatus(contextB, "error", "description 3", "url 3"), 200 }, 201 expectedDiffs: []github.Status{}, 202 }, 203 { 204 name: "only have context B", 205 start: []github.Status{ 206 makeStatus(contextB, "failure", "description 1", "url 1"), 207 }, 208 expectedDiffs: []github.Status{}, 209 }, 210 { 211 name: "unrelated contexts; context B but not A", 212 start: []github.Status{ 213 makeStatus("unrelated context", "success", "description 2", "url 2"), 214 makeStatus(contextB, "failure", "description 1", "url 1"), 215 makeStatus("also not related", "error", "description 4", "url 4"), 216 }, 217 expectedDiffs: []github.Status{}, 218 }, 219 { 220 name: "no contexts", 221 start: []github.Status{}, 222 expectedDiffs: []github.Status{}, 223 }, 224 } 225 226 m := *CopyMode(contextA, contextB) 227 for _, test := range tests { 228 diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start}) 229 if err := compareDiffs(diff, test.expectedDiffs); err != nil { 230 t.Errorf("CopyMode test '%s' %v\n", test.name, err) 231 } 232 } 233 } 234 235 func TestRetireModeReplacement(t *testing.T) { 236 contextA := "context A" 237 contextB := "context B" 238 desc := "Context retired. Status moved to \"context B\"." 239 240 tests := []*modeTest{ 241 { 242 name: "simple", 243 start: []github.Status{ 244 makeStatus(contextA, "failure", "description 1", "url 1"), 245 makeStatus(contextB, "failure", "description 1", "url 1"), 246 }, 247 expectedDiffs: []github.Status{ 248 makeStatus(contextA, "success", desc, ""), 249 }, 250 }, 251 { 252 name: "unrelated contexts;updated context B", 253 start: []github.Status{ 254 makeStatus("unrelated context", "success", "description 2", "url 2"), 255 makeStatus(contextA, "failure", "description 1", "url 1"), 256 makeStatus("also not related", "error", "description 4", "url 4"), 257 makeStatus(contextB, "success", "description 3", "url 3"), 258 }, 259 expectedDiffs: []github.Status{ 260 makeStatus(contextA, "success", desc, ""), 261 }, 262 }, 263 { 264 name: "missing context B", 265 start: []github.Status{ 266 makeStatus(contextA, "failure", "description 1", "url 1"), 267 }, 268 expectedDiffs: []github.Status{}, 269 }, 270 { 271 name: "unrelated contexts;missing context B", 272 start: []github.Status{ 273 makeStatus("unrelated context", "success", "description 2", "url 2"), 274 makeStatus(contextA, "failure", "description 1", "url 1"), 275 makeStatus("also not related", "error", "description 4", "url 4"), 276 }, 277 expectedDiffs: []github.Status{}, 278 }, 279 { 280 name: "missing context A", 281 start: []github.Status{ 282 makeStatus(contextB, "failure", "description 1", "url 1"), 283 }, 284 expectedDiffs: []github.Status{}, 285 }, 286 { 287 name: "unrelated contexts;missing context A", 288 start: []github.Status{ 289 makeStatus("unrelated context", "success", "description 2", "url 2"), 290 makeStatus("also not related", "error", "description 4", "url 4"), 291 makeStatus(contextB, "success", "description 3", "url 3"), 292 }, 293 expectedDiffs: []github.Status{}, 294 }, 295 { 296 name: "no contexts", 297 start: []github.Status{}, 298 expectedDiffs: []github.Status{}, 299 }, 300 } 301 302 m := *RetireMode(contextA, contextB, "") 303 for _, test := range tests { 304 diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start}) 305 if err := compareDiffs(diff, test.expectedDiffs); err != nil { 306 t.Errorf("RetireMode(Replacement) test '%s' %v\n", test.name, err) 307 } 308 } 309 } 310 311 func TestRetireModeNoReplacement(t *testing.T) { 312 contextA := "context A" 313 desc := "Context retired without replacement." 314 315 tests := []*modeTest{ 316 { 317 name: "simple", 318 start: []github.Status{ 319 makeStatus(contextA, "failure", "description 1", "url 1"), 320 }, 321 expectedDiffs: []github.Status{ 322 makeStatus(contextA, "success", desc, ""), 323 }, 324 }, 325 { 326 name: "unrelated contexts", 327 start: []github.Status{ 328 makeStatus("unrelated context", "success", "description 2", "url 2"), 329 makeStatus(contextA, "failure", "description 1", "url 1"), 330 makeStatus("also not related", "error", "description 4", "url 4"), 331 }, 332 expectedDiffs: []github.Status{ 333 makeStatus(contextA, "success", desc, ""), 334 }, 335 }, 336 { 337 name: "missing context A", 338 start: []github.Status{}, 339 expectedDiffs: []github.Status{}, 340 }, 341 { 342 name: "unrelated contexts;missing context A", 343 start: []github.Status{ 344 makeStatus("unrelated context", "success", "description 2", "url 2"), 345 makeStatus("also not related", "error", "description 4", "url 4"), 346 }, 347 expectedDiffs: []github.Status{}, 348 }, 349 } 350 351 m := *RetireMode(contextA, "", "") 352 for _, test := range tests { 353 diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start}) 354 if err := compareDiffs(diff, test.expectedDiffs); err != nil { 355 t.Errorf("RetireMode(NoReplace) test '%s' %v\n", test.name, err) 356 } 357 } 358 } 359 360 // makeStatus returns a new Status struct with the specified fields. 361 // targetURL=="" means TargetURL==nil 362 func makeStatus(context, state, description, targetURL string) github.Status { 363 var url string 364 if targetURL != "" { 365 url = targetURL 366 } 367 return github.Status{ 368 Context: context, 369 State: state, 370 Description: description, 371 TargetURL: url, 372 } 373 } 374 375 type refID struct { 376 org, repo, ref string 377 } 378 379 type fakeGitHubClient struct { 380 statusesRetrieved map[refID]interface{} 381 } 382 383 func (c *fakeGitHubClient) GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) { 384 c.statusesRetrieved[refID{org: org, repo: repo, ref: ref}] = nil 385 return nil, errors.New("return error to stop execution early") 386 } 387 388 func (c *fakeGitHubClient) CreateStatus(org, repo, SHA string, s github.Status) error { 389 return nil 390 } 391 392 func (c *fakeGitHubClient) GetPullRequests(org, repo string) ([]github.PullRequest, error) { 393 return []github.PullRequest{}, nil 394 } 395 396 func TestProcessPR(t *testing.T) { 397 var testCases = []struct { 398 name string 399 matches bool 400 }{ 401 { 402 name: "branch matching filter should proceed", 403 matches: true, 404 }, 405 { 406 name: "branch not matching filter should not proceed", 407 matches: false, 408 }, 409 } 410 411 for _, testCase := range testCases { 412 client := fakeGitHubClient{statusesRetrieved: map[refID]interface{}{}} 413 var filteredBranch string 414 migrator := Migrator{ 415 org: "org", 416 repo: "repo", 417 targetBranchFilter: func(branch string) bool { 418 filteredBranch = branch 419 return testCase.matches 420 }, 421 client: &client, 422 } 423 migrator.processPR(github.PullRequest{Base: github.PullRequestBranch{Ref: "branch"}, Head: github.PullRequestBranch{SHA: "fake"}}) 424 if filteredBranch != "branch" { 425 t.Errorf("%s: failed to use filter on branch", testCase.name) 426 } 427 428 _, retrieved := client.statusesRetrieved[refID{org: "org", repo: "repo", ref: "fake"}] 429 if testCase.matches && !retrieved { 430 t.Errorf("%s: failed to process a PR that matched", testCase.name) 431 } 432 if !testCase.matches && retrieved { 433 t.Errorf("%s: processed a PR that didn't match", testCase.name) 434 } 435 } 436 }