github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/blockade/blockade_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 blockade 18 19 import ( 20 "fmt" 21 "reflect" 22 "regexp" 23 "sort" 24 "strings" 25 "testing" 26 27 "github.com/sirupsen/logrus" 28 29 "sigs.k8s.io/prow/pkg/config" 30 "sigs.k8s.io/prow/pkg/github" 31 "sigs.k8s.io/prow/pkg/github/fakegithub" 32 "sigs.k8s.io/prow/pkg/labels" 33 "sigs.k8s.io/prow/pkg/plugins" 34 ) 35 36 var ( 37 // Sample changes: 38 docFile = github.PullRequestChange{Filename: "docs/documentation.md", BlobURL: "<URL1>"} 39 docOwners = github.PullRequestChange{Filename: "docs/OWNERS", BlobURL: "<URL2>"} 40 docOwners2 = github.PullRequestChange{Filename: "docs/2/OWNERS", BlobURL: "<URL3>"} 41 srcGo = github.PullRequestChange{Filename: "src/code.go", BlobURL: "<URL4>"} 42 srcSh = github.PullRequestChange{Filename: "src/shell.sh", BlobURL: "<URL5>"} 43 docSh = github.PullRequestChange{Filename: "docs/shell.sh", BlobURL: "<URL6>"} 44 conformanceYaml = github.PullRequestChange{Filename: "test/conformance/testdata/conformance.yaml", BlobURL: "<URL6>"} 45 46 // branches 47 releaseBranchRegexp = "^release-*" 48 releaseBranchRe = regexp.MustCompile(releaseBranchRegexp) 49 50 // Sample blockades: 51 blockDocs = plugins.Blockade{ 52 Repos: []string{"org/repo"}, 53 BlockRegexps: []string{`docs/.*`}, 54 Explanation: "1", 55 } 56 blockDocsExceptOwners = plugins.Blockade{ 57 Repos: []string{"org/repo"}, 58 BlockRegexps: []string{`docs/.*`}, 59 ExceptionRegexps: []string{`.*OWNERS`}, 60 Explanation: "2", 61 } 62 blockShell = plugins.Blockade{ 63 Repos: []string{"org/repo"}, 64 BlockRegexps: []string{`.*\.sh`}, 65 Explanation: "3", 66 } 67 blockAllOrg = plugins.Blockade{ 68 Repos: []string{"org"}, 69 BlockRegexps: []string{`.*`}, 70 Explanation: "4", 71 } 72 blockAllOther = plugins.Blockade{ 73 Repos: []string{"org2"}, 74 BlockRegexps: []string{`.*`}, 75 Explanation: "5", 76 } 77 blockConformanceOnReleaseBranch = plugins.Blockade{ 78 Repos: []string{"org/repo"}, 79 BranchRegexp: &releaseBranchRegexp, 80 BranchRe: releaseBranchRe, 81 BlockRegexps: []string{`test/conformance/testdata/.*`}, 82 Explanation: "6", 83 } 84 blockBadBranchRe = plugins.Blockade{ 85 Repos: []string{"org/repo"}, 86 BranchRegexp: &releaseBranchRegexp, 87 BlockRegexps: []string{`test/conformance/testdata/.*`}, 88 Explanation: "6", 89 } 90 ) 91 92 // TestCalculateBlocks validates that changes are blocked or allowed correctly. 93 func TestCalculateBlocks(t *testing.T) { 94 tcs := []struct { 95 name string 96 branch string 97 changes []github.PullRequestChange 98 config []plugins.Blockade 99 expectedSummary summary 100 }{ 101 { 102 name: "nil BranchRe", 103 config: []plugins.Blockade{blockBadBranchRe}, 104 changes: []github.PullRequestChange{}, 105 expectedSummary: summary{}, 106 }, 107 { 108 name: "blocked by 1/1 blockade (no exceptions), extra file", 109 config: []plugins.Blockade{blockDocs}, 110 changes: []github.PullRequestChange{docFile, docOwners, srcGo}, 111 expectedSummary: summary{ 112 "1": []github.PullRequestChange{docFile, docOwners}, 113 }, 114 }, 115 { 116 name: "blocked by 1/1 blockade (1/2 files are exceptions), extra file", 117 config: []plugins.Blockade{blockDocsExceptOwners}, 118 changes: []github.PullRequestChange{docFile, docOwners, srcGo}, 119 expectedSummary: summary{ 120 "2": []github.PullRequestChange{docFile}, 121 }, 122 }, 123 { 124 name: "blocked by 0/1 blockades (2/2 exceptions), extra file", 125 config: []plugins.Blockade{blockDocsExceptOwners}, 126 changes: []github.PullRequestChange{docOwners, docOwners2, srcGo}, 127 expectedSummary: summary{}, 128 }, 129 { 130 name: "blocked by 0/1 blockades (no exceptions), extra file", 131 config: []plugins.Blockade{blockDocsExceptOwners}, 132 changes: []github.PullRequestChange{srcGo, srcSh}, 133 expectedSummary: summary{}, 134 }, 135 { 136 name: "blocked by 2/2 blockades (no exceptions), extra file", 137 config: []plugins.Blockade{blockDocsExceptOwners, blockShell}, 138 changes: []github.PullRequestChange{srcGo, srcSh, docFile}, 139 expectedSummary: summary{ 140 "2": []github.PullRequestChange{docFile}, 141 "3": []github.PullRequestChange{srcSh}, 142 }, 143 }, 144 { 145 name: "blocked by 2/2 blockades w/ single file", 146 config: []plugins.Blockade{blockDocsExceptOwners, blockShell}, 147 changes: []github.PullRequestChange{docSh}, 148 expectedSummary: summary{ 149 "2": []github.PullRequestChange{docSh}, 150 "3": []github.PullRequestChange{docSh}, 151 }, 152 }, 153 { 154 name: "blocked by 2/2 blockades w/ single file (1/2 exceptions)", 155 config: []plugins.Blockade{blockDocsExceptOwners, blockShell}, 156 changes: []github.PullRequestChange{docSh, docOwners}, 157 expectedSummary: summary{ 158 "2": []github.PullRequestChange{docSh}, 159 "3": []github.PullRequestChange{docSh}, 160 }, 161 }, 162 { 163 name: "blocked by 1/2 blockades (1/2 exceptions), extra file", 164 config: []plugins.Blockade{blockDocsExceptOwners, blockShell}, 165 changes: []github.PullRequestChange{srcSh, docOwners, srcGo}, 166 expectedSummary: summary{ 167 "3": []github.PullRequestChange{srcSh}, 168 }, 169 }, 170 { 171 name: "blocked by 0/2 blockades (1/2 exceptions), extra file", 172 config: []plugins.Blockade{blockDocsExceptOwners, blockShell}, 173 changes: []github.PullRequestChange{docOwners, srcGo}, 174 expectedSummary: summary{}, 175 }, 176 { 177 name: "blocked by 1/1 blockade on release branch w/ single file", 178 branch: "release-1.20", 179 config: []plugins.Blockade{blockConformanceOnReleaseBranch}, 180 changes: []github.PullRequestChange{conformanceYaml}, 181 expectedSummary: summary{ 182 "6": []github.PullRequestChange{conformanceYaml}, 183 }, 184 }, 185 { 186 name: "don't block conformance on main branch", 187 branch: "main", 188 config: []plugins.Blockade{blockConformanceOnReleaseBranch}, 189 changes: []github.PullRequestChange{conformanceYaml}, 190 expectedSummary: summary{}, 191 }, 192 { 193 name: "blocked by 2/2 blockades on release branch (no exceptions), extra file", 194 branch: "release-1.20", 195 config: []plugins.Blockade{blockConformanceOnReleaseBranch, blockDocsExceptOwners}, 196 changes: []github.PullRequestChange{conformanceYaml, docFile, srcGo}, 197 expectedSummary: summary{ 198 "2": []github.PullRequestChange{docFile}, 199 "6": []github.PullRequestChange{conformanceYaml}, 200 }, 201 }, 202 } 203 204 for _, tc := range tcs { 205 blockades := compileApplicableBlockades("org", "repo", tc.branch, logrus.WithField("plugin", PluginName), tc.config) 206 sum := calculateBlocks(tc.changes, blockades) 207 if !reflect.DeepEqual(sum, tc.expectedSummary) { 208 t.Errorf("[%s] Expected summary: %#v, actual summary: %#v.", tc.name, tc.expectedSummary, sum) 209 } 210 } 211 } 212 213 func TestSummaryString(t *testing.T) { 214 // Just one example for now. 215 tcs := []struct { 216 name string 217 sum summary 218 expectedContents []string 219 }{ 220 { 221 name: "Simple example", 222 sum: summary{ 223 "reason A": []github.PullRequestChange{docFile}, 224 "reason B": []github.PullRequestChange{srcGo, srcSh}, 225 }, 226 expectedContents: []string{ 227 "#### Reasons for blocking this PR:\n", 228 "[reason A]\n- [docs/documentation.md](<URL1>)\n\n", 229 "[reason B]\n- [src/code.go](<URL4>)\n\n- [src/shell.sh](<URL5>)\n\n", 230 }, 231 }, 232 } 233 234 for _, tc := range tcs { 235 got := tc.sum.String() 236 for _, expected := range tc.expectedContents { 237 if !strings.Contains(got, expected) { 238 t.Errorf("[%s] Expected summary %#v to contain %q, but got %q.", tc.name, tc.sum, expected, got) 239 } 240 } 241 } 242 } 243 244 func formatLabel(label string) string { 245 return fmt.Sprintf("%s/%s#%d:%s", "org", "repo", 1, label) 246 } 247 248 type fakePruner struct{} 249 250 func (f *fakePruner) PruneComments(_ func(ic github.IssueComment) bool) {} 251 252 // TestHandle validates that: 253 // - The correct labels are added/removed. 254 // - A comment is created when needed. 255 // - Uninteresting events are ignored. 256 // - Blockades that don't apply to this repo are ignored. 257 func TestHandle(t *testing.T) { 258 // Don't need to validate the following because they are validated by other tests: 259 // - Block calculation. (Whether or not changes justify blocking the PR.) 260 // - Comment contents, just existence. 261 otherLabel := labels.LGTM 262 263 tcs := []struct { 264 name string 265 action github.PullRequestEventAction 266 config []plugins.Blockade 267 hasLabel bool 268 filesBlock bool // This is ignored if there are no applicable blockades for the repo. 269 270 labelAdded string 271 labelRemoved string 272 commentCreated bool 273 }{ 274 { 275 name: "Boring action", 276 action: github.PullRequestActionEdited, 277 config: []plugins.Blockade{blockDocsExceptOwners}, 278 hasLabel: false, 279 filesBlock: true, 280 }, 281 { 282 name: "Basic block", 283 action: github.PullRequestActionOpened, 284 config: []plugins.Blockade{blockDocsExceptOwners}, 285 hasLabel: false, 286 filesBlock: true, 287 288 labelAdded: labels.BlockedPaths, 289 commentCreated: true, 290 }, 291 { 292 name: "Basic block, already labeled", 293 action: github.PullRequestActionOpened, 294 config: []plugins.Blockade{blockDocsExceptOwners}, 295 hasLabel: true, 296 filesBlock: true, 297 }, 298 { 299 name: "Not blocked, not labeled", 300 action: github.PullRequestActionOpened, 301 config: []plugins.Blockade{blockDocsExceptOwners}, 302 hasLabel: false, 303 filesBlock: false, 304 }, 305 { 306 name: "Not blocked, has label", 307 action: github.PullRequestActionOpened, 308 config: []plugins.Blockade{blockDocsExceptOwners}, 309 hasLabel: true, 310 filesBlock: false, 311 312 labelRemoved: labels.BlockedPaths, 313 }, 314 { 315 name: "No blockade, not labeled", 316 action: github.PullRequestActionOpened, 317 config: []plugins.Blockade{}, 318 hasLabel: false, 319 filesBlock: true, 320 }, 321 { 322 name: "No blockade, has label", 323 action: github.PullRequestActionOpened, 324 config: []plugins.Blockade{}, 325 hasLabel: true, 326 filesBlock: true, 327 328 labelRemoved: labels.BlockedPaths, 329 }, 330 { 331 name: "Basic block (org scoped blockade)", 332 action: github.PullRequestActionOpened, 333 config: []plugins.Blockade{blockAllOrg}, 334 hasLabel: false, 335 filesBlock: true, 336 337 labelAdded: labels.BlockedPaths, 338 commentCreated: true, 339 }, 340 { 341 name: "Would be blocked, but blockade is not applicable; not labeled", 342 action: github.PullRequestActionOpened, 343 config: []plugins.Blockade{blockAllOther}, 344 hasLabel: false, 345 filesBlock: true, 346 }, 347 } 348 349 for _, tc := range tcs { 350 var expectAdded []string 351 fakeClient := fakegithub.NewFakeClient() 352 fakeClient.RepoLabelsExisting = []string{labels.BlockedPaths, otherLabel} 353 if tc.hasLabel { 354 label := formatLabel(labels.BlockedPaths) 355 fakeClient.IssueLabelsAdded = append(fakeClient.IssueLabelsAdded, label) 356 expectAdded = append(expectAdded, label) 357 } 358 calcF := func(_ []github.PullRequestChange, blockades []blockade) summary { 359 if !tc.filesBlock { 360 return nil 361 } 362 sum := make(summary) 363 for _, b := range blockades { 364 // For this test assume 'docFile' is blocked by every blockade that is applicable to the repo. 365 sum[b.explanation] = []github.PullRequestChange{docFile} 366 } 367 return sum 368 } 369 pre := &github.PullRequestEvent{ 370 Action: tc.action, 371 Repo: github.Repo{Owner: github.User{Login: "org"}, Name: "repo"}, 372 Number: 1, 373 } 374 if err := handle(fakeClient, logrus.WithField("plugin", PluginName), tc.config, &fakePruner{}, calcF, pre); err != nil { 375 t.Errorf("[%s] Unexpected error from handle: %v.", tc.name, err) 376 continue 377 } 378 379 if tc.labelAdded != "" { 380 expectAdded = append(expectAdded, formatLabel(tc.labelAdded)) 381 } 382 sort.Strings(expectAdded) 383 sort.Strings(fakeClient.IssueLabelsAdded) 384 if !reflect.DeepEqual(expectAdded, fakeClient.IssueLabelsAdded) { 385 t.Errorf("[%s]: Expected labels to be added: %q, but got: %q.", tc.name, expectAdded, fakeClient.IssueLabelsAdded) 386 } 387 var expectRemoved []string 388 if tc.labelRemoved != "" { 389 expectRemoved = append(expectRemoved, formatLabel(tc.labelRemoved)) 390 } 391 sort.Strings(expectRemoved) 392 sort.Strings(fakeClient.IssueLabelsRemoved) 393 if !reflect.DeepEqual(expectRemoved, fakeClient.IssueLabelsRemoved) { 394 t.Errorf("[%s]: Expected labels to be removed: %q, but got: %q.", tc.name, expectRemoved, fakeClient.IssueLabelsRemoved) 395 } 396 397 if count := len(fakeClient.IssueComments[1]); count > 1 { 398 t.Errorf("[%s] More than 1 comment created! (%d created).", tc.name, count) 399 } else if (count == 1) != tc.commentCreated { 400 t.Errorf("[%s] Expected comment created: %t, but got %t.", tc.name, tc.commentCreated, count == 1) 401 } 402 } 403 } 404 405 func TestHelpProvider(t *testing.T) { 406 enabledRepos := []config.OrgRepo{ 407 {Org: "org1", Repo: "repo"}, 408 {Org: "org2", Repo: "repo"}, 409 } 410 cases := []struct { 411 name string 412 config *plugins.Configuration 413 enabledRepos []config.OrgRepo 414 err bool 415 }{ 416 { 417 name: "Empty config", 418 config: &plugins.Configuration{}, 419 enabledRepos: enabledRepos, 420 }, 421 { 422 name: "All configs enabled", 423 config: &plugins.Configuration{ 424 Blockades: []plugins.Blockade{ 425 { 426 Repos: []string{"org2/repo"}, 427 BranchRegexp: &releaseBranchRegexp, 428 BlockRegexps: []string{"no", "nope"}, 429 ExceptionRegexps: []string{"except", "exceptional"}, 430 Explanation: "Because I have decided so.", 431 }, 432 }, 433 }, 434 enabledRepos: enabledRepos, 435 }, 436 } 437 for _, c := range cases { 438 t.Run(c.name, func(t *testing.T) { 439 _, err := helpProvider(c.config, c.enabledRepos) 440 if err != nil && !c.err { 441 t.Fatalf("helpProvider error: %v", err) 442 } 443 }) 444 } 445 }