k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/label_sync/main_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 main 18 19 import ( 20 "encoding/json" 21 "strings" 22 "testing" 23 "time" 24 25 "github.com/google/go-cmp/cmp" 26 "github.com/google/go-cmp/cmp/cmpopts" 27 ) 28 29 // Tests for getting data from GitHub are not needed: 30 // The would have to use real API point or test stubs 31 32 // Test func (c Configuration) validate(orgs string) error 33 // Input: Configuration list 34 func TestValidate(t *testing.T) { 35 var testcases = []struct { 36 name string 37 config Configuration 38 expectedError bool 39 }{ 40 { 41 name: "All empty", 42 }, 43 { 44 name: "Duplicate wanted label", 45 config: Configuration{Default: RepoConfig{Labels: []Label{ 46 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 47 {Name: "lab1", Description: "Test Label 1", Color: "befade"}, 48 }}}, 49 expectedError: true, 50 }, 51 { 52 name: "Required label has non unique labels when downcased", 53 config: Configuration{Default: RepoConfig{Labels: []Label{ 54 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 55 {Name: "LAB1", Description: "Test Label 2", Color: "deadbe"}, 56 }}}, 57 expectedError: true, 58 }, 59 { 60 name: "Required label defined in default and repo1", 61 config: Configuration{ 62 Default: RepoConfig{Labels: []Label{ 63 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 64 }}, 65 Repos: map[string]RepoConfig{ 66 "org/repo1": {Labels: []Label{ 67 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 68 }}, 69 }, 70 }, 71 expectedError: true, 72 }, 73 { 74 name: "Org2 not in orgs, should warn in logs", 75 config: Configuration{ 76 Default: RepoConfig{Labels: []Label{ 77 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 78 }}, 79 Repos: map[string]RepoConfig{ 80 "org2/repo1": {Labels: []Label{ 81 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 82 }}, 83 }, 84 }, 85 expectedError: false, 86 }, 87 { 88 name: "Required label defined in default and org", 89 config: Configuration{ 90 Default: RepoConfig{Labels: []Label{ 91 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 92 }}, 93 Orgs: map[string]RepoConfig{ 94 "org": {Labels: []Label{ 95 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 96 }}, 97 }, 98 }, 99 expectedError: true, 100 }, 101 { 102 name: "Required label defined in org and repo", 103 config: Configuration{ 104 Orgs: map[string]RepoConfig{ 105 "org": {Labels: []Label{ 106 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 107 }}, 108 }, 109 Repos: map[string]RepoConfig{ 110 "org/repo1": {Labels: []Label{ 111 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 112 }}, 113 }, 114 }, 115 expectedError: true, 116 }, 117 } 118 // Do tests 119 for _, tc := range testcases { 120 err := tc.config.validate("org") 121 if err == nil && tc.expectedError { 122 t.Errorf("%s: failed to raise error", tc.name) 123 } else if err != nil && !tc.expectedError { 124 t.Errorf("%s: unexpected error: %v", tc.name, err) 125 } 126 } 127 } 128 129 // Test syncLabels(config *Configuration, curr *RepoLabels) (updates RepoUpdates, err error) 130 // Input: Configuration list and Current labels list on multiple repos 131 // Output: list of wanted label updates (update due to name or color) addition due to missing labels 132 // This is main testing for this program 133 func TestSyncLabels(t *testing.T) { 134 var testcases = []struct { 135 name string 136 config Configuration 137 current RepoLabels 138 expectedUpdates RepoUpdates 139 expectedError bool 140 now time.Time 141 }{ 142 { 143 name: "Required label defined in repo1 and repo2 - no update", 144 config: Configuration{ 145 Default: RepoConfig{Labels: []Label{ 146 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 147 }}, 148 Repos: map[string]RepoConfig{ 149 "org/repo1": {Labels: []Label{ 150 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 151 }}, 152 "org/repo2": {Labels: []Label{ 153 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 154 }}, 155 }, 156 }, 157 current: RepoLabels{ 158 "repo1": { 159 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 160 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 161 }, 162 "repo2": { 163 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 164 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 165 }, 166 }, 167 }, 168 { 169 name: "Required label defined in repo1 and repo2 - update required", 170 config: Configuration{ 171 Default: RepoConfig{Labels: []Label{ 172 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 173 }}, 174 Repos: map[string]RepoConfig{ 175 "org/repo1": {Labels: []Label{ 176 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 177 }}, 178 "org/repo2": {Labels: []Label{ 179 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 180 }}, 181 }, 182 }, 183 current: RepoLabels{ 184 "repo1": { 185 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 186 }, 187 "repo2": { 188 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 189 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 190 }, 191 }, 192 expectedUpdates: RepoUpdates{ 193 "repo1": { 194 {repo: "repo1", Why: "missing", Wanted: &Label{Name: "lab1", Description: "Test Label 1", Color: "deadbe"}}}, 195 }, 196 }, 197 { 198 name: "Required label defined on org-level - update required on one repo", 199 config: Configuration{ 200 Default: RepoConfig{Labels: []Label{ 201 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 202 }}, 203 Orgs: map[string]RepoConfig{ 204 "org": {Labels: []Label{ 205 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 206 }}, 207 }, 208 }, 209 current: RepoLabels{ 210 "repo1": { 211 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 212 {Name: "lab2", Description: "Test Label 2", Color: "deadbe"}, 213 }, 214 "repo2": { 215 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 216 }, 217 }, 218 expectedUpdates: RepoUpdates{ 219 "repo2": { 220 {repo: "repo2", Why: "missing", Wanted: &Label{Name: "lab2", Description: "Test Label 2", Color: "deadbe"}}}, 221 }, 222 }, 223 { 224 name: "Duplicate label on repo1", 225 current: RepoLabels{ 226 "repo1": { 227 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 228 {Name: "lab1", Description: "Test Label 1", Color: "befade"}, 229 }, 230 }, 231 expectedError: true, 232 }, 233 { 234 name: "Non unique label on repo1 when downcased", 235 current: RepoLabels{ 236 "repo1": { 237 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 238 {Name: "LAB1", Description: "Test Label 2", Color: "deadbe"}, 239 }, 240 }, 241 expectedError: true, 242 }, 243 { 244 name: "Non unique label but on different repos - allowed", 245 current: RepoLabels{ 246 "repo1": {{Name: "lab1", Description: "Test Label 1", Color: "deadbe"}}, 247 "repo2": {{Name: "lab1", Description: "Test Label 1", Color: "deadbe"}}, 248 }, 249 }, 250 { 251 name: "Repo has exactly all wanted labels", 252 config: Configuration{Default: RepoConfig{Labels: []Label{ 253 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 254 }}}, 255 current: RepoLabels{ 256 "repo1": { 257 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 258 }, 259 }, 260 }, 261 { 262 name: "Repo has label with wrong color", 263 config: Configuration{Default: RepoConfig{Labels: []Label{ 264 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 265 }}}, 266 current: RepoLabels{ 267 "repo1": { 268 {Name: "lab1", Description: "Test Label 1", Color: "bebeef"}, 269 }, 270 }, 271 expectedUpdates: RepoUpdates{ 272 "repo1": { 273 {Why: "change", Current: &Label{Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, Wanted: &Label{Name: "lab1", Description: "Test Label 1", Color: "deadbe"}}, 274 }, 275 }, 276 }, 277 { 278 name: "Repo has label with wrong description", 279 config: Configuration{Default: RepoConfig{Labels: []Label{ 280 {Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, 281 }}}, 282 current: RepoLabels{ 283 "repo1": { 284 {Name: "lab1", Description: "Test Label 5", Color: "deadbe"}, 285 }, 286 }, 287 expectedUpdates: RepoUpdates{ 288 "repo1": { 289 {Why: "change", Current: &Label{Name: "lab1", Description: "Test Label 1", Color: "deadbe"}, Wanted: &Label{Name: "lab1", Description: "Test Label 1", Color: "deadbe"}}, 290 }, 291 }, 292 }, 293 { 294 name: "Repo has label with wrong name (different case)", 295 config: Configuration{Default: RepoConfig{Labels: []Label{ 296 {Name: "Lab1", Description: "Test Label 1", Color: "deadbe"}, 297 }}}, 298 current: RepoLabels{ 299 "repo1": { 300 {Name: "laB1", Description: "Test Label 1", Color: "deadbe"}, 301 }, 302 }, 303 expectedUpdates: RepoUpdates{ 304 "repo1": { 305 {Why: "rename", Wanted: &Label{Name: "Lab1", Description: "Test Label 1", Color: "deadbe"}, Current: &Label{Name: "laB1", Description: "Test Label 1", Color: "deadbe"}}, 306 }, 307 }, 308 }, 309 { 310 name: "old name", 311 config: Configuration{Default: RepoConfig{Labels: []Label{ 312 {Name: "current", Description: "Test Label 1", Color: "blue", Previously: []Label{{Name: "old", Description: "Test Label 1", Color: "gray"}}}, 313 }}}, 314 current: RepoLabels{ 315 "no current": {{Name: "old", Description: "Test Label 1", Color: "much gray"}}, 316 "has current": { 317 {Name: "old", Description: "Test Label 1", Color: "gray"}, 318 {Name: "current", Description: "Test Label 1", Color: "blue"}, 319 }, 320 }, 321 expectedUpdates: RepoUpdates{ 322 "no current": { 323 {Why: "rename", Current: &Label{Name: "old", Description: "Test Label 1", Color: "much gray"}, Wanted: &Label{Name: "current", Description: "Test Label 1", Color: "blue"}}, 324 }, 325 "has current": { 326 {Why: "migrate", Current: &Label{Name: "old", Description: "Test Label 1", Color: "gray"}, Wanted: &Label{Name: "current", Description: "Test Label 1", Color: "blue"}}, 327 }, 328 }, 329 }, 330 { 331 name: "Repo is missing a label", 332 config: Configuration{Default: RepoConfig{Labels: []Label{ 333 {Name: "Lab1", Description: "Test Label 1", Color: "deadbe"}, 334 }}}, 335 current: RepoLabels{ 336 "repo1": {}, 337 }, 338 expectedUpdates: RepoUpdates{ 339 "repo1": { 340 {Why: "missing", Wanted: &Label{Name: "Lab1", Description: "Test Label 1", Color: "deadbe"}}, 341 }, 342 }, 343 }, 344 { 345 name: "Repo is missing multiple labels, and expected labels order is changed", 346 config: Configuration{Default: RepoConfig{Labels: []Label{ 347 {Name: "Lab1", Description: "Test Label 1", Color: "deadbe"}, 348 {Name: "Lab2", Description: "Test Label 2", Color: "000000"}, 349 {Name: "Lab3", Description: "Test Label 3", Color: "ffffff"}, 350 }}}, 351 current: RepoLabels{ 352 "repo1": {}, 353 "repo2": {{Name: "Lab2", Description: "Test Label 2", Color: "000000"}}, 354 }, 355 expectedUpdates: RepoUpdates{ 356 "repo2": { 357 {Why: "missing", Wanted: &Label{Name: "Lab3", Description: "Test Label 3", Color: "ffffff"}}, 358 {Why: "missing", Wanted: &Label{Name: "Lab1", Description: "Test Label 1", Color: "deadbe"}}, 359 }, 360 "repo1": { 361 {Why: "missing", Wanted: &Label{Color: "000000", Name: "Lab2", Description: "Test Label 2"}}, 362 {Why: "missing", Wanted: &Label{Name: "Lab3", Description: "Test Label 3", Color: "ffffff"}}, 363 {Why: "missing", Wanted: &Label{Name: "Lab1", Description: "Test Label 1", Color: "deadbe"}}, 364 }, 365 }, 366 }, 367 { 368 name: "Multiple repos complex case", 369 config: Configuration{Default: RepoConfig{Labels: []Label{ 370 {Name: "priority/P0", Description: "P0 Priority", Color: "ff0000"}, 371 {Name: "lgtm", Description: "LGTM", Color: "00ff00"}, 372 }}}, 373 current: RepoLabels{ 374 "repo1": { 375 {Name: "Priority/P0", Description: "P0 Priority", Color: "ee3333"}, 376 {Name: "LGTM", Description: "LGTM", Color: "00ff00"}, 377 }, 378 "repo2": { 379 {Name: "priority/P0", Description: "P0 Priority", Color: "ee3333"}, 380 {Name: "lgtm", Description: "LGTM", Color: "00ff00"}, 381 }, 382 "repo3": { 383 {Name: "PRIORITY/P0", Description: "P0 Priority", Color: "ff0000"}, 384 {Name: "lgtm", Description: "LGTM", Color: "0000ff"}, 385 }, 386 "repo4": { 387 {Name: "priority/P0", Description: "P0 Priority", Color: "ff0000"}, 388 }, 389 "repo5": { 390 {Name: "lgtm", Description: "LGTM", Color: "00ff00"}, 391 }, 392 }, 393 expectedUpdates: RepoUpdates{ 394 "repo1": { 395 {Why: "rename", Wanted: &Label{Name: "priority/P0", Description: "P0 Priority", Color: "ff0000"}, Current: &Label{Name: "Priority/P0", Description: "P0 Priority", Color: "ee3333"}}, 396 {Why: "rename", Wanted: &Label{Name: "lgtm", Description: "LGTM", Color: "00ff00"}, Current: &Label{Name: "LGTM", Description: "LGTM", Color: "00ff00"}}, 397 }, 398 "repo2": { 399 {Why: "change", Current: &Label{Name: "priority/P0", Description: "P0 Priority", Color: "ff0000"}, Wanted: &Label{Name: "priority/P0", Description: "P0 Priority", Color: "ff0000"}}, 400 }, 401 "repo3": { 402 {Why: "rename", Wanted: &Label{Name: "priority/P0", Description: "P0 Priority", Color: "ff0000"}, Current: &Label{Name: "PRIORITY/P0", Description: "P0 Priority", Color: "ff0000"}}, 403 {Why: "change", Current: &Label{Name: "lgtm", Description: "LGTM", Color: "00ff00"}, Wanted: &Label{Name: "lgtm", Description: "LGTM", Color: "00ff00"}}, 404 }, 405 "repo4": { 406 {Why: "missing", Wanted: &Label{Name: "lgtm", Description: "LGTM", Color: "00ff00"}}, 407 }, 408 "repo5": { 409 {Why: "missing", Wanted: &Label{Name: "priority/P0", Description: "P0 Priority", Color: "ff0000"}}, 410 }, 411 }, 412 }, 413 } 414 415 // Do tests 416 for _, tc := range testcases { 417 actualUpdates, err := syncLabels(tc.config, "org", tc.current) 418 if err == nil && tc.expectedError { 419 t.Errorf("%s: failed to raise error", tc.name) 420 } else if err != nil && !tc.expectedError { 421 t.Errorf("%s: unexpected error: %v", tc.name, err) 422 } else if !tc.expectedError && !equalUpdates(actualUpdates, tc.expectedUpdates, t) { 423 t.Errorf("%s: expected updates:\n%+v\ngot:\n%+v", tc.name, tc.expectedUpdates, actualUpdates) 424 } 425 } 426 } 427 428 // This is needed to compare Update sets, two update sets are equal 429 // only if their maps have the same lists (but order can be different) 430 // Using standard `reflect.DeepEqual` for entire structures makes tests flaky 431 func equalUpdates(updates1, updates2 RepoUpdates, t *testing.T) bool { 432 if len(updates1) != len(updates2) { 433 t.Errorf("ERROR: expected and actual update sets have different repo sets") 434 return false 435 } 436 // Iterate per repository differences 437 for repo, list1 := range updates1 { 438 list2, ok := updates2[repo] 439 if !ok || len(list1) != len(list2) { 440 t.Errorf("ERROR: expected and actual update lists for repo %s have different lengths", repo) 441 return false 442 } 443 items1 := make(map[string]bool) 444 for _, item := range list1 { 445 j, err := json.Marshal(item) 446 if err != nil { 447 t.Errorf("ERROR: internal test error: unable to json.Marshal test item: %+v", item) 448 return false 449 } 450 items1[string(j)] = true 451 } 452 items2 := make(map[string]bool) 453 for _, item := range list2 { 454 j, err := json.Marshal(item) 455 if err != nil { 456 t.Errorf("ERROR: internal test error: unable to json.Marshal test item: %+v", item) 457 return false 458 } 459 items2[string(j)] = true 460 } 461 // Iterate list of label differences 462 for key := range items1 { 463 _, ok := items2[key] 464 if !ok { 465 t.Errorf("ERROR: difference: repo: %s, key: %s not found", repo, key) 466 return false 467 } 468 } 469 } 470 return true 471 } 472 473 // Test loading YAML file (labels.yaml) 474 func TestLoadYAML(t *testing.T) { 475 d := time.Date(2017, 1, 1, 13, 0, 0, 0, time.UTC) 476 var testcases = []struct { 477 path string 478 expected Configuration 479 ok bool 480 errMsg string 481 }{ 482 { 483 path: "labels_example.yaml", 484 expected: Configuration{ 485 Default: RepoConfig{Labels: []Label{ 486 {Name: "lgtm", Description: "LGTM", Color: "green"}, 487 {Name: "priority/P0", Description: "P0 Priority", Color: "red", Previously: []Label{{Name: "P0", Description: "P0 Priority", Color: "blue"}}}, 488 {Name: "dead-label", Description: "Delete Me :)", DeleteAfter: &d}, 489 }}, 490 Orgs: map[string]RepoConfig{"org": {Labels: []Label{{Name: "sgtm", Description: "Sounds Good To Me", Color: "green"}}}}, 491 Repos: map[string]RepoConfig{"org/repo": {Labels: []Label{{Name: "tgtm", Description: "Tastes Good To Me", Color: "blue"}}}}, 492 }, 493 ok: true, 494 }, 495 { 496 path: "syntax_error_example.yaml", 497 expected: Configuration{}, 498 ok: false, 499 errMsg: "error converting", 500 }, 501 { 502 path: "no_such_file.yaml", 503 expected: Configuration{}, 504 ok: false, 505 errMsg: "no such file", 506 }, 507 } 508 for i, tc := range testcases { 509 actual, err := LoadConfig(tc.path, "org") 510 errNil := err == nil 511 if errNil != tc.ok { 512 t.Errorf("TestLoadYAML: test case number %d, expected ok: %v, got %v (error=%v)", i+1, tc.ok, err == nil, err) 513 } 514 if !errNil && !strings.Contains(err.Error(), tc.errMsg) { 515 t.Errorf("TestLoadYAML: test case number %d, expected error '%v' to contain '%v'", i+1, err.Error(), tc.errMsg) 516 } 517 if diff := cmp.Diff(actual, &tc.expected, cmpopts.IgnoreUnexported(Label{})); errNil && diff != "" { 518 t.Errorf("TestLoadYAML: test case number %d, labels differ:%s", i+1, diff) 519 } 520 } 521 }