github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/tide/status_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 tide 18 19 import ( 20 "fmt" 21 "strings" 22 "testing" 23 24 githubql "github.com/shurcooL/githubv4" 25 "github.com/sirupsen/logrus" 26 27 "k8s.io/apimachinery/pkg/util/sets" 28 "k8s.io/test-infra/prow/config" 29 "k8s.io/test-infra/prow/github" 30 ) 31 32 func TestExpectedStatus(t *testing.T) { 33 neededLabels := []string{"need-1", "need-2", "need-a-very-super-duper-extra-not-short-at-all-label-name"} 34 forbiddenLabels := []string{"forbidden-1", "forbidden-2"} 35 testcases := []struct { 36 name string 37 38 baseref string 39 branchWhitelist []string 40 branchBlacklist []string 41 sameBranchReqs bool 42 labels []string 43 milestone string 44 contexts []Context 45 inPool bool 46 47 state string 48 desc string 49 }{ 50 { 51 name: "in pool", 52 inPool: true, 53 54 state: github.StatusSuccess, 55 desc: statusInPool, 56 }, 57 { 58 name: "check truncation of label list", 59 milestone: "v1.0", 60 inPool: false, 61 62 state: github.StatusPending, 63 desc: fmt.Sprintf(statusNotInPool, " Needs need-1, need-2 labels."), 64 }, 65 { 66 name: "check truncation of label list is not excessive", 67 labels: append([]string{}, neededLabels[:2]...), 68 milestone: "v1.0", 69 inPool: false, 70 71 state: github.StatusPending, 72 desc: fmt.Sprintf(statusNotInPool, " Needs need-a-very-super-duper-extra-not-short-at-all-label-name label."), 73 }, 74 { 75 name: "has forbidden labels", 76 labels: append(append([]string{}, neededLabels...), forbiddenLabels...), 77 milestone: "v1.0", 78 inPool: false, 79 80 state: github.StatusPending, 81 desc: fmt.Sprintf(statusNotInPool, " Should not have forbidden-1, forbidden-2 labels."), 82 }, 83 { 84 name: "has one forbidden label", 85 labels: append(append([]string{}, neededLabels...), forbiddenLabels[0]), 86 milestone: "v1.0", 87 inPool: false, 88 89 state: github.StatusPending, 90 desc: fmt.Sprintf(statusNotInPool, " Should not have forbidden-1 label."), 91 }, 92 { 93 name: "only mention one requirement class", 94 labels: append(append([]string{}, neededLabels[1:]...), forbiddenLabels[0]), 95 milestone: "v1.0", 96 inPool: false, 97 98 state: github.StatusPending, 99 desc: fmt.Sprintf(statusNotInPool, " Needs need-1 label."), 100 }, 101 { 102 name: "against excluded branch", 103 baseref: "bad", 104 branchBlacklist: []string{"bad"}, 105 sameBranchReqs: true, 106 labels: neededLabels, 107 inPool: false, 108 109 state: github.StatusPending, 110 desc: fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."), 111 }, 112 { 113 name: "not against included branch", 114 baseref: "bad", 115 branchWhitelist: []string{"good"}, 116 sameBranchReqs: true, 117 labels: neededLabels, 118 inPool: false, 119 120 state: github.StatusPending, 121 desc: fmt.Sprintf(statusNotInPool, " Merging to branch bad is forbidden."), 122 }, 123 { 124 name: "choose query for correct branch", 125 baseref: "bad", 126 branchWhitelist: []string{"good"}, 127 milestone: "v1.0", 128 labels: neededLabels, 129 inPool: false, 130 131 state: github.StatusPending, 132 desc: fmt.Sprintf(statusNotInPool, " Needs 1, 2, 3, 4, 5, 6, 7 labels."), 133 }, 134 { 135 name: "only failed tide context", 136 labels: neededLabels, 137 milestone: "v1.0", 138 contexts: []Context{{Context: githubql.String(statusContext), State: githubql.StatusStateError}}, 139 inPool: false, 140 141 state: github.StatusPending, 142 desc: fmt.Sprintf(statusNotInPool, ""), 143 }, 144 { 145 name: "single bad context", 146 labels: neededLabels, 147 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateError}}, 148 milestone: "v1.0", 149 inPool: false, 150 151 state: github.StatusPending, 152 desc: fmt.Sprintf(statusNotInPool, " Job job-name has not succeeded."), 153 }, 154 { 155 name: "multiple bad contexts", 156 labels: neededLabels, 157 milestone: "v1.0", 158 contexts: []Context{ 159 {Context: githubql.String("job-name"), State: githubql.StatusStateError}, 160 {Context: githubql.String("other-job-name"), State: githubql.StatusStateError}, 161 }, 162 inPool: false, 163 164 state: github.StatusPending, 165 desc: fmt.Sprintf(statusNotInPool, " Jobs job-name, other-job-name have not succeeded."), 166 }, 167 { 168 name: "wrong milestone", 169 labels: neededLabels, 170 milestone: "v1.1", 171 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}}, 172 inPool: false, 173 174 state: github.StatusPending, 175 desc: fmt.Sprintf(statusNotInPool, " Must be in milestone v1.0."), 176 }, 177 { 178 name: "unknown requirement", 179 labels: neededLabels, 180 milestone: "v1.0", 181 contexts: []Context{{Context: githubql.String("job-name"), State: githubql.StatusStateSuccess}}, 182 inPool: false, 183 184 state: github.StatusPending, 185 desc: fmt.Sprintf(statusNotInPool, ""), 186 }, 187 { 188 name: "check that min diff query is used", 189 labels: []string{"3", "4", "5", "6", "7"}, 190 milestone: "v1.0", 191 inPool: false, 192 193 state: github.StatusPending, 194 desc: fmt.Sprintf(statusNotInPool, " Needs 1, 2 labels."), 195 }, 196 } 197 198 for _, tc := range testcases { 199 t.Logf("Test Case: %q\n", tc.name) 200 secondQuery := config.TideQuery{ 201 Orgs: []string{""}, 202 Labels: []string{"1", "2", "3", "4", "5", "6", "7"}, // lots of requirements 203 Milestone: "v1.0", 204 } 205 if tc.sameBranchReqs { 206 secondQuery.ExcludedBranches = tc.branchBlacklist 207 secondQuery.IncludedBranches = tc.branchWhitelist 208 } 209 queriesByRepo := config.TideQueries{ 210 config.TideQuery{ 211 Orgs: []string{""}, 212 ExcludedBranches: tc.branchBlacklist, 213 IncludedBranches: tc.branchWhitelist, 214 Labels: neededLabels, 215 MissingLabels: forbiddenLabels, 216 Milestone: "v1.0", 217 }, 218 secondQuery, 219 }.QueryMap() 220 var pr PullRequest 221 pr.BaseRef = struct { 222 Name githubql.String 223 Prefix githubql.String 224 }{ 225 Name: githubql.String(tc.baseref), 226 } 227 for _, label := range tc.labels { 228 pr.Labels.Nodes = append( 229 pr.Labels.Nodes, 230 struct{ Name githubql.String }{Name: githubql.String(label)}, 231 ) 232 } 233 if len(tc.contexts) > 0 { 234 pr.HeadRefOID = githubql.String("head") 235 pr.Commits.Nodes = append( 236 pr.Commits.Nodes, 237 struct{ Commit Commit }{ 238 Commit: Commit{ 239 Status: struct{ Contexts []Context }{ 240 Contexts: tc.contexts, 241 }, 242 OID: githubql.String("head"), 243 }, 244 }, 245 ) 246 } 247 if tc.milestone != "" { 248 pr.Milestone = &struct { 249 Title githubql.String 250 }{githubql.String(tc.milestone)} 251 } 252 var pool map[string]PullRequest 253 if tc.inPool { 254 pool = map[string]PullRequest{"#0": {}} 255 } 256 257 state, desc := expectedStatus(queriesByRepo, &pr, pool, &config.TideContextPolicy{}) 258 if state != tc.state { 259 t.Errorf("Expected status state %q, but got %q.", string(tc.state), string(state)) 260 } 261 if desc != tc.desc { 262 t.Errorf("Expected status description %q, but got %q.", tc.desc, desc) 263 } 264 } 265 } 266 267 func TestSetStatuses(t *testing.T) { 268 statusNotInPoolEmpty := fmt.Sprintf(statusNotInPool, "") 269 testcases := []struct { 270 name string 271 272 inPool bool 273 hasContext bool 274 state githubql.StatusState 275 desc string 276 277 shouldSet bool 278 }{ 279 { 280 name: "in pool with proper context", 281 282 inPool: true, 283 hasContext: true, 284 state: githubql.StatusStateSuccess, 285 desc: statusInPool, 286 287 shouldSet: false, 288 }, 289 { 290 name: "in pool without context", 291 292 inPool: true, 293 hasContext: false, 294 295 shouldSet: true, 296 }, 297 { 298 name: "in pool with improper context", 299 300 inPool: true, 301 hasContext: true, 302 state: githubql.StatusStateSuccess, 303 desc: statusNotInPoolEmpty, 304 305 shouldSet: true, 306 }, 307 { 308 name: "in pool with wrong state", 309 310 inPool: true, 311 hasContext: true, 312 state: githubql.StatusStatePending, 313 desc: statusInPool, 314 315 shouldSet: true, 316 }, 317 { 318 name: "not in pool with proper context", 319 320 inPool: false, 321 hasContext: true, 322 state: githubql.StatusStatePending, 323 desc: statusNotInPoolEmpty, 324 325 shouldSet: false, 326 }, 327 { 328 name: "not in pool with improper context", 329 330 inPool: false, 331 hasContext: true, 332 state: githubql.StatusStatePending, 333 desc: statusInPool, 334 335 shouldSet: true, 336 }, 337 { 338 name: "not in pool with no context", 339 340 inPool: false, 341 hasContext: false, 342 343 shouldSet: true, 344 }, 345 } 346 for _, tc := range testcases { 347 var pr PullRequest 348 pr.Commits.Nodes = []struct{ Commit Commit }{{}} 349 if tc.hasContext { 350 pr.Commits.Nodes[0].Commit.Status.Contexts = []Context{ 351 { 352 Context: githubql.String(statusContext), 353 State: tc.state, 354 Description: githubql.String(tc.desc), 355 }, 356 } 357 } 358 pool := make(map[string]PullRequest) 359 if tc.inPool { 360 pool[prKey(&pr)] = pr 361 } 362 fc := &fgc{} 363 ca := &config.Agent{} 364 ca.Set(&config.Config{}) 365 // setStatuses logs instead of returning errors. 366 // Construct a logger to watch for errors to be printed. 367 log := logrus.WithField("component", "tide") 368 initialLog, err := log.String() 369 if err != nil { 370 t.Fatalf("Failed to get log output before testing: %v", err) 371 } 372 373 sc := &statusController{ghc: fc, ca: ca, logger: log} 374 sc.setStatuses([]PullRequest{pr}, pool) 375 if str, err := log.String(); err != nil { 376 t.Fatalf("For case %s: failed to get log output: %v", tc.name, err) 377 } else if str != initialLog { 378 t.Errorf("For case %s: error setting status: %s", tc.name, str) 379 } 380 if tc.shouldSet && !fc.setStatus { 381 t.Errorf("For case %s: should set but didn't", tc.name) 382 } else if !tc.shouldSet && fc.setStatus { 383 t.Errorf("For case %s: should not set but did", tc.name) 384 } 385 } 386 } 387 388 func TestTargetUrl(t *testing.T) { 389 testcases := []struct { 390 name string 391 pr *PullRequest 392 config config.Tide 393 394 expectedURL string 395 }{ 396 { 397 name: "no config", 398 pr: &PullRequest{}, 399 config: config.Tide{}, 400 expectedURL: "", 401 }, 402 { 403 name: "tide overview config", 404 pr: &PullRequest{}, 405 config: config.Tide{TargetURL: "tide.com"}, 406 expectedURL: "tide.com", 407 }, 408 { 409 name: "PR dashboard config and overview config", 410 pr: &PullRequest{}, 411 config: config.Tide{TargetURL: "tide.com", PRStatusBaseURL: "pr.status.com"}, 412 expectedURL: "tide.com", 413 }, 414 { 415 name: "PR dashboard config", 416 pr: &PullRequest{ 417 Author: struct { 418 Login githubql.String 419 }{Login: githubql.String("author")}, 420 Repository: struct { 421 Name githubql.String 422 NameWithOwner githubql.String 423 Owner struct { 424 Login githubql.String 425 } 426 }{NameWithOwner: githubql.String("org/repo")}, 427 HeadRefName: "head", 428 }, 429 config: config.Tide{PRStatusBaseURL: "pr.status.com"}, 430 expectedURL: "pr.status.com?query=is%3Apr+repo%3Aorg%2Frepo+author%3Aauthor+head%3Ahead", 431 }, 432 } 433 434 for _, tc := range testcases { 435 ca := &config.Agent{} 436 ca.Set(&config.Config{ProwConfig: config.ProwConfig{Tide: tc.config}}) 437 log := logrus.WithField("controller", "status-update") 438 if actual, expected := targetURL(ca, tc.pr, log), tc.expectedURL; actual != expected { 439 t.Errorf("%s: expected target URL %s but got %s", tc.name, expected, actual) 440 } 441 } 442 } 443 444 func TestOpenPRsQuery(t *testing.T) { 445 var q string 446 checkTok := func(tok string) { 447 if !strings.Contains(q, " "+tok+" ") { 448 t.Errorf("Expected query to contain \"%s\", got \"%s\"", tok, q) 449 } 450 } 451 452 orgs := []string{"org", "kuber"} 453 repos := []string{"k8s/k8s", "k8s/t-i"} 454 exceptions := map[string]sets.String{ 455 "org": sets.NewString("org/repo1", "org/repo2"), 456 "irrelevant-org": sets.NewString("irrelevant-org/repo1", "irrelevant-org/repo2"), 457 } 458 459 q = " " + openPRsQuery(orgs, repos, exceptions) + " " 460 checkTok("is:pr") 461 checkTok("state:open") 462 checkTok("org:\"org\"") 463 checkTok("org:\"kuber\"") 464 checkTok("repo:\"k8s/k8s\"") 465 checkTok("repo:\"k8s/t-i\"") 466 checkTok("-repo:\"org/repo1\"") 467 checkTok("-repo:\"org/repo2\"") 468 }