github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/tide/blockers/blockers_test.go (about) 1 /* 2 Copyright 2018 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 blockers 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "strconv" 24 "sync" 25 "testing" 26 27 "github.com/google/go-cmp/cmp" 28 "github.com/google/go-cmp/cmp/cmpopts" 29 githubql "github.com/shurcooL/githubv4" 30 "github.com/sirupsen/logrus" 31 32 "k8s.io/apimachinery/pkg/util/sets" 33 ) 34 35 func TestParseBranches(t *testing.T) { 36 tcs := []struct { 37 text string 38 expected []string 39 }{ 40 { 41 text: "", 42 expected: nil, 43 }, 44 { 45 text: "BAD THINGS (all branches blocked)", 46 expected: nil, 47 }, 48 { 49 text: "branch:foo", 50 expected: []string{"foo"}, 51 }, 52 { 53 text: "branch: foo-bar", 54 expected: []string{"foo-bar"}, 55 }, 56 { 57 text: "BAD THINGS (BLOCKING BRANCH:foo branch:bar) AHHH", 58 expected: []string{"foo", "bar"}, 59 }, 60 { 61 text: "branch:\"FOO-bar\"", 62 expected: []string{"FOO-bar"}, 63 }, 64 { 65 text: "branch: \"foo\" branch: \"bar\"", 66 expected: []string{"foo", "bar"}, 67 }, 68 } 69 70 for _, tc := range tcs { 71 if got := parseBranches(tc.text); !reflect.DeepEqual(got, tc.expected) { 72 t.Errorf("Expected parseBranches(%q)==%q, but got %q.", tc.text, tc.expected, got) 73 } 74 } 75 } 76 77 func TestBlockerQuery(t *testing.T) { 78 tcs := []struct { 79 orgRepoQuery string 80 expected sets.Set[string] 81 }{ 82 { 83 orgRepoQuery: "org:\"k8s\"", 84 expected: sets.New[string]( 85 "is:issue", 86 "state:open", 87 "label:\"blocker\"", 88 "org:\"k8s\"", 89 ), 90 }, 91 { 92 orgRepoQuery: "repo:\"k8s/t-i\"", 93 expected: sets.New[string]( 94 "is:issue", 95 "state:open", 96 "label:\"blocker\"", 97 "repo:\"k8s/t-i\"", 98 ), 99 }, 100 { 101 orgRepoQuery: "org:\"k8s\" org:\"kuber\"", 102 expected: sets.New[string]( 103 "is:issue", 104 "state:open", 105 "label:\"blocker\"", 106 "org:\"k8s\"", 107 "org:\"kuber\"", 108 ), 109 }, 110 { 111 orgRepoQuery: "repo:\"k8s/t-i\" repo:\"k8s/k8s\"", 112 expected: sets.New[string]( 113 "is:issue", 114 "state:open", 115 "label:\"blocker\"", 116 "repo:\"k8s/t-i\"", 117 "repo:\"k8s/k8s\"", 118 ), 119 }, 120 { 121 orgRepoQuery: "org:\"k8s\" org:\"kuber\" repo:\"k8s/t-i\" repo:\"k8s/k8s\"", 122 expected: sets.New[string]( 123 "is:issue", 124 "state:open", 125 "label:\"blocker\"", 126 "repo:\"k8s/t-i\"", 127 "repo:\"k8s/k8s\"", 128 "org:\"k8s\"", 129 "org:\"kuber\"", 130 ), 131 }, 132 } 133 134 for _, tc := range tcs { 135 got := sets.New[string](blockerQuery("blocker", tc.orgRepoQuery)...) 136 if diff := cmp.Diff(got, tc.expected); diff != "" { 137 t.Errorf("Actual result differs from expected: %s", diff) 138 } 139 } 140 } 141 142 func testIssue(number int, title, org, repo string) Issue { 143 return Issue{ 144 Number: githubql.Int(number), 145 Title: githubql.String(title), 146 URL: githubql.String(strconv.Itoa(number)), 147 Repository: struct { 148 Name githubql.String 149 Owner struct { 150 Login githubql.String 151 } 152 }{ 153 Name: githubql.String(repo), 154 Owner: struct { 155 Login githubql.String 156 }{ 157 Login: githubql.String(org), 158 }, 159 }, 160 } 161 } 162 163 func TestBlockers(t *testing.T) { 164 type check struct { 165 org, repo, branch string 166 blockers sets.Set[int] 167 } 168 169 tcs := []struct { 170 name string 171 issues []Issue 172 checks []check 173 }{ 174 { 175 name: "No blocker issues", 176 issues: []Issue{}, 177 checks: []check{ 178 { 179 org: "org", 180 repo: "repo", 181 branch: "branch", 182 blockers: sets.New[int](), 183 }, 184 }, 185 }, 186 { 187 name: "1 repo blocker", 188 issues: []Issue{ 189 testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"), 190 }, 191 checks: []check{ 192 { 193 org: "k", 194 repo: "t-i", 195 branch: "feature", 196 blockers: sets.New[int](5), 197 }, 198 { 199 org: "k", 200 repo: "t-i", 201 branch: "master", 202 blockers: sets.New[int](5), 203 }, 204 { 205 org: "k", 206 repo: "k", 207 branch: "master", 208 blockers: sets.New[int](), 209 }, 210 }, 211 }, 212 { 213 name: "1 repo blocker for a branch", 214 issues: []Issue{ 215 testIssue(6, "BLOCK THE release-1.11 BRANCH! branch:release-1.11", "k", "t-i"), 216 }, 217 checks: []check{ 218 { 219 org: "k", 220 repo: "t-i", 221 branch: "release-1.11", 222 blockers: sets.New[int](6), 223 }, 224 }, 225 }, 226 { 227 name: "1 repo blocker for a branch", 228 issues: []Issue{ 229 testIssue(6, "BLOCK THE slash/in/name BRANCH! branch:slash/in/name", "k", "t-i"), 230 }, 231 checks: []check{ 232 { 233 org: "k", 234 repo: "t-i", 235 branch: "slash/in/name", 236 blockers: sets.New[int](6), 237 }, 238 }, 239 }, 240 { 241 name: "2 repo blockers for same repo", 242 issues: []Issue{ 243 testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"), 244 testIssue(6, "BLOCK THE WHOLE REPO AGAIN!", "k", "t-i"), 245 }, 246 checks: []check{ 247 { 248 org: "k", 249 repo: "t-i", 250 branch: "feature", 251 blockers: sets.New[int](5, 6), 252 }, 253 { 254 org: "k", 255 repo: "t-i", 256 branch: "master", 257 blockers: sets.New[int](5, 6), 258 }, 259 { 260 org: "k", 261 repo: "k", 262 branch: "master", 263 blockers: sets.New[int](), 264 }, 265 }, 266 }, 267 { 268 name: "2 repo blockers for different repos", 269 issues: []Issue{ 270 testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"), 271 testIssue(6, "BLOCK THE WHOLE (different) REPO!", "k", "community"), 272 }, 273 checks: []check{ 274 { 275 org: "k", 276 repo: "t-i", 277 branch: "feature", 278 blockers: sets.New[int](5), 279 }, 280 { 281 org: "k", 282 repo: "t-i", 283 branch: "master", 284 blockers: sets.New[int](5), 285 }, 286 { 287 org: "k", 288 repo: "community", 289 branch: "feature", 290 blockers: sets.New[int](6), 291 }, 292 { 293 org: "k", 294 repo: "community", 295 branch: "master", 296 blockers: sets.New[int](6), 297 }, 298 { 299 org: "k", 300 repo: "k", 301 branch: "master", 302 blockers: sets.New[int](), 303 }, 304 }, 305 }, 306 { 307 name: "1 repo blocker, 1 branch blocker for different repos", 308 issues: []Issue{ 309 testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"), 310 testIssue(6, "BLOCK THE feature BRANCH! branch:feature", "k", "community"), 311 }, 312 checks: []check{ 313 { 314 org: "k", 315 repo: "t-i", 316 branch: "feature", 317 blockers: sets.New[int](5), 318 }, 319 { 320 org: "k", 321 repo: "t-i", 322 branch: "master", 323 blockers: sets.New[int](5), 324 }, 325 { 326 org: "k", 327 repo: "community", 328 branch: "feature", 329 blockers: sets.New[int](6), 330 }, 331 { 332 org: "k", 333 repo: "community", 334 branch: "master", 335 blockers: sets.New[int](), 336 }, 337 { 338 org: "k", 339 repo: "k", 340 branch: "master", 341 blockers: sets.New[int](), 342 }, 343 }, 344 }, 345 { 346 name: "1 repo blocker, 1 branch blocker for same repo", 347 issues: []Issue{ 348 testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"), 349 testIssue(6, "BLOCK THE feature BRANCH! branch:feature", "k", "t-i"), 350 }, 351 checks: []check{ 352 { 353 org: "k", 354 repo: "t-i", 355 branch: "feature", 356 blockers: sets.New[int](5, 6), 357 }, 358 { 359 org: "k", 360 repo: "t-i", 361 branch: "master", 362 blockers: sets.New[int](5), 363 }, 364 { 365 org: "k", 366 repo: "k", 367 branch: "master", 368 blockers: sets.New[int](), 369 }, 370 }, 371 }, 372 { 373 name: "2 repo blockers, 3 branch blockers (with overlap) for same repo", 374 issues: []Issue{ 375 testIssue(5, "BLOCK THE WHOLE REPO!", "k", "t-i"), 376 testIssue(6, "BLOCK THE WHOLE REPO AGAIN!", "k", "t-i"), 377 testIssue(7, "BLOCK THE feature BRANCH! branch:feature", "k", "t-i"), 378 testIssue(8, "BLOCK THE feature BRANCH! branch:master", "k", "t-i"), 379 testIssue(9, "BLOCK THE feature BRANCH! branch:feature branch: master branch:foo", "k", "t-i"), 380 }, 381 checks: []check{ 382 { 383 org: "k", 384 repo: "t-i", 385 branch: "feature", 386 blockers: sets.New[int](5, 6, 7, 9), 387 }, 388 { 389 org: "k", 390 repo: "t-i", 391 branch: "master", 392 blockers: sets.New[int](5, 6, 8, 9), 393 }, 394 { 395 org: "k", 396 repo: "t-i", 397 branch: "foo", 398 blockers: sets.New[int](5, 6, 9), 399 }, 400 { 401 org: "k", 402 repo: "t-i", 403 branch: "bar", 404 blockers: sets.New[int](5, 6), 405 }, 406 { 407 org: "k", 408 repo: "k", 409 branch: "master", 410 blockers: sets.New[int](), 411 }, 412 }, 413 }, 414 } 415 416 for _, tc := range tcs { 417 t.Logf("Running test case %q.", tc.name) 418 b := fromIssues(tc.issues, logrus.WithField("test", tc.name)) 419 for _, c := range tc.checks { 420 actuals := b.GetApplicable(c.org, c.repo, c.branch) 421 nums := sets.New[int]() 422 for _, actual := range actuals { 423 // Check blocker URLs: 424 if actual.URL != strconv.Itoa(actual.Number) { 425 t.Errorf("blocker %d has URL %q, expected %q", actual.Number, actual.URL, strconv.Itoa(actual.Number)) 426 } 427 nums.Insert(actual.Number) 428 } 429 // Check that correct blockers were selected: 430 if !reflect.DeepEqual(nums, c.blockers) { 431 t.Errorf("expected blockers %v, but got %v", c.blockers, nums) 432 } 433 } 434 } 435 } 436 437 type fakeGitHubClient struct { 438 lock sync.Mutex 439 queries map[string][]string 440 } 441 442 func (fghc *fakeGitHubClient) QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error { 443 if query := vars["query"]; query == nil || string(query.(githubql.String)) == "" { 444 return fmt.Errorf("query variable was unset, variables: %+v", vars) 445 } 446 447 fghc.lock.Lock() 448 defer fghc.lock.Unlock() 449 450 if fghc.queries == nil { 451 fghc.queries = map[string][]string{} 452 } 453 fghc.queries[org] = append(fghc.queries[org], string(vars["query"].(githubql.String))) 454 455 return nil 456 } 457 458 func TestBlockersFindAll(t *testing.T) { 459 t.Parallel() 460 461 orgRepoTokensByOrg := map[string]string{ 462 "org-a": `org:"org-a" -repo:"org-a/repo-b"`, 463 "org-b": `org:"org-b" -repo:"org-b/repo-b"`, 464 } 465 const blockerLabel = "tide/merge-blocker" 466 testCases := []struct { 467 name string 468 usesAppsAuth bool 469 470 expectedQueries map[string][]string 471 }{ 472 { 473 name: "Apps auth, query is split by org", 474 usesAppsAuth: true, 475 expectedQueries: map[string][]string{ 476 "org-a": {`-repo:"org-a/repo-b" is:issue label:"tide/merge-blocker" org:"org-a" state:open`}, 477 "org-b": {`-repo:"org-b/repo-b" is:issue label:"tide/merge-blocker" org:"org-b" state:open`}, 478 }, 479 }, 480 { 481 name: "No apps auth, one query", 482 usesAppsAuth: false, 483 expectedQueries: map[string][]string{ 484 "": {`-repo:"org-a/repo-b" -repo:"org-b/repo-b" is:issue label:"tide/merge-blocker" org:"org-a" org:"org-b" state:open`}, 485 }, 486 }, 487 } 488 489 for _, tc := range testCases { 490 t.Run(tc.name, func(t *testing.T) { 491 ghc := &fakeGitHubClient{} 492 493 if _, err := FindAll(ghc, logrus.WithField("tc", tc.name), blockerLabel, orgRepoTokensByOrg, tc.usesAppsAuth); err != nil { 494 t.Fatalf("FindAll: %v", err) 495 } 496 497 if diff := cmp.Diff(ghc.queries, tc.expectedQueries, cmpopts.SortSlices(func(a, b string) bool { return a < b })); diff != "" { 498 t.Errorf("actual queries differ from expected: %v", diff) 499 } 500 }) 501 } 502 }