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  }