sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/statusreconciler/migrator/migrator_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 migrator
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"testing"
    23  
    24  	"sigs.k8s.io/prow/pkg/github"
    25  )
    26  
    27  type modeTest struct {
    28  	name          string
    29  	start         []github.Status
    30  	expectedDiffs []github.Status
    31  }
    32  
    33  // compareDiffs checks if a list of status updates matches an expected list of status updates.
    34  func compareDiffs(diffs []github.Status, expectedDiffs []github.Status) error {
    35  	if len(diffs) != len(expectedDiffs) {
    36  		return fmt.Errorf("failed because the returned diff had %d changes instead of %d", len(diffs), len(expectedDiffs))
    37  	}
    38  	for _, diff := range diffs {
    39  		if diff.Context == "" {
    40  			return fmt.Errorf("failed because the returned diff contained a Status with an empty Context field")
    41  		}
    42  		if diff.Description == "" {
    43  			return fmt.Errorf("failed because the returned diff contained a Status with an empty Description field")
    44  		}
    45  		if diff.State == "" {
    46  			return fmt.Errorf("failed because the returned diff contained a Status with an empty State field")
    47  		}
    48  		var match github.Status
    49  		var found bool
    50  		for _, expected := range expectedDiffs {
    51  			if expected.Context == diff.Context {
    52  				match = expected
    53  				found = true
    54  				break
    55  			}
    56  		}
    57  		if !found {
    58  			return fmt.Errorf("failed because the returned diff contained an unexpected change to context '%s'", diff.Context)
    59  		}
    60  		// Found a matching context. Make sure that fields are equal.
    61  		if match.Description != diff.Description {
    62  			return fmt.Errorf("failed because the returned diff for context '%s' had Description '%s' instead of '%s'", diff.Context, diff.Description, match.Description)
    63  		}
    64  		if match.State != diff.State {
    65  			return fmt.Errorf("failed because the returned diff for context '%s' had State '%s' instead of '%s'", diff.Context, diff.State, match.State)
    66  		}
    67  
    68  		if match.TargetURL == "" {
    69  			if diff.TargetURL != "" {
    70  				return fmt.Errorf("failed because the returned diff for context '%s' had a non-empty TargetURL", diff.Context)
    71  			}
    72  		} else if diff.TargetURL == "" {
    73  			return fmt.Errorf("failed because the returned diff for context '%s' had an empty TargetURL", diff.Context)
    74  		} else if match.TargetURL != diff.TargetURL {
    75  			return fmt.Errorf("failed because the returned diff for context '%s' had TargetURL '%s' instead of '%s'", diff.Context, diff.TargetURL, match.TargetURL)
    76  		}
    77  	}
    78  	return nil
    79  }
    80  
    81  func TestMoveMode(t *testing.T) {
    82  	contextA := "context A"
    83  	contextB := "context B"
    84  	desc := "Context retired. Status moved to \"context B\"."
    85  
    86  	tests := []*modeTest{
    87  		{
    88  			name: "simple",
    89  			start: []github.Status{
    90  				makeStatus(contextA, "failure", "description 1", "url 1"),
    91  			},
    92  			expectedDiffs: []github.Status{
    93  				makeStatus(contextA, "success", desc, ""),
    94  				makeStatus(contextB, "failure", "description 1", "url 1"),
    95  			},
    96  		},
    97  		{
    98  			name: "unrelated contexts",
    99  			start: []github.Status{
   100  				makeStatus("also not related", "error", "description 4", "url 4"),
   101  				makeStatus(contextA, "failure", "description 1", "url 1"),
   102  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   103  			},
   104  			expectedDiffs: []github.Status{
   105  				makeStatus(contextA, "success", desc, ""),
   106  				makeStatus(contextB, "failure", "description 1", "url 1"),
   107  			},
   108  		},
   109  		{
   110  			name: "unrelated contexts; missing context A",
   111  			start: []github.Status{
   112  				makeStatus("also not related", "error", "description 4", "url 4"),
   113  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   114  			},
   115  			expectedDiffs: []github.Status{},
   116  		},
   117  		{
   118  			name: "unrelated contexts; already have context A and B",
   119  			start: []github.Status{
   120  				makeStatus("also not related", "error", "description 4", "url 4"),
   121  				makeStatus(contextA, "failure", "description 1", "url 1"),
   122  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   123  				makeStatus(contextB, "failure", "description 1", "url 1"),
   124  			},
   125  			expectedDiffs: []github.Status{},
   126  		},
   127  		{
   128  			name: "unrelated contexts; already have context B; no context A",
   129  			start: []github.Status{
   130  				makeStatus("also not related", "error", "description 4", "url 4"),
   131  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   132  				makeStatus(contextB, "failure", "description 1", "url 1"),
   133  			},
   134  			expectedDiffs: []github.Status{},
   135  		},
   136  		{
   137  			name:          "no contexts",
   138  			start:         []github.Status{},
   139  			expectedDiffs: []github.Status{},
   140  		},
   141  	}
   142  
   143  	m := *MoveMode(contextA, contextB, "")
   144  	for _, test := range tests {
   145  		diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start})
   146  		if err := compareDiffs(diff, test.expectedDiffs); err != nil {
   147  			t.Errorf("MoveMode test '%s' %v\n", test.name, err)
   148  		}
   149  	}
   150  }
   151  
   152  func TestCopyMode(t *testing.T) {
   153  	contextA := "context A"
   154  	contextB := "context B"
   155  
   156  	tests := []*modeTest{
   157  		{
   158  			name: "simple",
   159  			start: []github.Status{
   160  				makeStatus(contextA, "failure", "description 1", "url 1"),
   161  			},
   162  			expectedDiffs: []github.Status{
   163  				makeStatus(contextB, "failure", "description 1", "url 1"),
   164  			},
   165  		},
   166  		{
   167  			name: "unrelated contexts",
   168  			start: []github.Status{
   169  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   170  				makeStatus(contextA, "failure", "description 1", "url 1"),
   171  				makeStatus("also not related", "error", "description 4", "url 4"),
   172  			},
   173  			expectedDiffs: []github.Status{
   174  				makeStatus(contextB, "failure", "description 1", "url 1"),
   175  			},
   176  		},
   177  		{
   178  			name: "already have context B",
   179  			start: []github.Status{
   180  				makeStatus(contextA, "failure", "description 1", "url 1"),
   181  				makeStatus(contextB, "failure", "description 1", "url 1"),
   182  			},
   183  			expectedDiffs: []github.Status{},
   184  		},
   185  		{
   186  			name: "already have updated context B",
   187  			start: []github.Status{
   188  				makeStatus(contextA, "failure", "description 1", "url 1"),
   189  				makeStatus(contextB, "success", "description 2", "url 2"),
   190  			},
   191  			expectedDiffs: []github.Status{},
   192  		},
   193  		{
   194  			name: "unrelated contexts already have updated context B",
   195  			start: []github.Status{
   196  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   197  				makeStatus(contextA, "failure", "description 1", "url 1"),
   198  				makeStatus("also not related", "error", "description 4", "url 4"),
   199  				makeStatus(contextB, "error", "description 3", "url 3"),
   200  			},
   201  			expectedDiffs: []github.Status{},
   202  		},
   203  		{
   204  			name: "only have context B",
   205  			start: []github.Status{
   206  				makeStatus(contextB, "failure", "description 1", "url 1"),
   207  			},
   208  			expectedDiffs: []github.Status{},
   209  		},
   210  		{
   211  			name: "unrelated contexts; context B but not A",
   212  			start: []github.Status{
   213  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   214  				makeStatus(contextB, "failure", "description 1", "url 1"),
   215  				makeStatus("also not related", "error", "description 4", "url 4"),
   216  			},
   217  			expectedDiffs: []github.Status{},
   218  		},
   219  		{
   220  			name:          "no contexts",
   221  			start:         []github.Status{},
   222  			expectedDiffs: []github.Status{},
   223  		},
   224  	}
   225  
   226  	m := *CopyMode(contextA, contextB)
   227  	for _, test := range tests {
   228  		diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start})
   229  		if err := compareDiffs(diff, test.expectedDiffs); err != nil {
   230  			t.Errorf("CopyMode test '%s' %v\n", test.name, err)
   231  		}
   232  	}
   233  }
   234  
   235  func TestRetireModeReplacement(t *testing.T) {
   236  	contextA := "context A"
   237  	contextB := "context B"
   238  	desc := "Context retired. Status moved to \"context B\"."
   239  
   240  	tests := []*modeTest{
   241  		{
   242  			name: "simple",
   243  			start: []github.Status{
   244  				makeStatus(contextA, "failure", "description 1", "url 1"),
   245  				makeStatus(contextB, "failure", "description 1", "url 1"),
   246  			},
   247  			expectedDiffs: []github.Status{
   248  				makeStatus(contextA, "success", desc, ""),
   249  			},
   250  		},
   251  		{
   252  			name: "unrelated contexts;updated context B",
   253  			start: []github.Status{
   254  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   255  				makeStatus(contextA, "failure", "description 1", "url 1"),
   256  				makeStatus("also not related", "error", "description 4", "url 4"),
   257  				makeStatus(contextB, "success", "description 3", "url 3"),
   258  			},
   259  			expectedDiffs: []github.Status{
   260  				makeStatus(contextA, "success", desc, ""),
   261  			},
   262  		},
   263  		{
   264  			name: "missing context B",
   265  			start: []github.Status{
   266  				makeStatus(contextA, "failure", "description 1", "url 1"),
   267  			},
   268  			expectedDiffs: []github.Status{},
   269  		},
   270  		{
   271  			name: "unrelated contexts;missing context B",
   272  			start: []github.Status{
   273  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   274  				makeStatus(contextA, "failure", "description 1", "url 1"),
   275  				makeStatus("also not related", "error", "description 4", "url 4"),
   276  			},
   277  			expectedDiffs: []github.Status{},
   278  		},
   279  		{
   280  			name: "missing context A",
   281  			start: []github.Status{
   282  				makeStatus(contextB, "failure", "description 1", "url 1"),
   283  			},
   284  			expectedDiffs: []github.Status{},
   285  		},
   286  		{
   287  			name: "unrelated contexts;missing context A",
   288  			start: []github.Status{
   289  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   290  				makeStatus("also not related", "error", "description 4", "url 4"),
   291  				makeStatus(contextB, "success", "description 3", "url 3"),
   292  			},
   293  			expectedDiffs: []github.Status{},
   294  		},
   295  		{
   296  			name:          "no contexts",
   297  			start:         []github.Status{},
   298  			expectedDiffs: []github.Status{},
   299  		},
   300  	}
   301  
   302  	m := *RetireMode(contextA, contextB, "")
   303  	for _, test := range tests {
   304  		diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start})
   305  		if err := compareDiffs(diff, test.expectedDiffs); err != nil {
   306  			t.Errorf("RetireMode(Replacement) test '%s' %v\n", test.name, err)
   307  		}
   308  	}
   309  }
   310  
   311  func TestRetireModeNoReplacement(t *testing.T) {
   312  	contextA := "context A"
   313  	desc := "Context retired without replacement."
   314  
   315  	tests := []*modeTest{
   316  		{
   317  			name: "simple",
   318  			start: []github.Status{
   319  				makeStatus(contextA, "failure", "description 1", "url 1"),
   320  			},
   321  			expectedDiffs: []github.Status{
   322  				makeStatus(contextA, "success", desc, ""),
   323  			},
   324  		},
   325  		{
   326  			name: "unrelated contexts",
   327  			start: []github.Status{
   328  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   329  				makeStatus(contextA, "failure", "description 1", "url 1"),
   330  				makeStatus("also not related", "error", "description 4", "url 4"),
   331  			},
   332  			expectedDiffs: []github.Status{
   333  				makeStatus(contextA, "success", desc, ""),
   334  			},
   335  		},
   336  		{
   337  			name:          "missing context A",
   338  			start:         []github.Status{},
   339  			expectedDiffs: []github.Status{},
   340  		},
   341  		{
   342  			name: "unrelated contexts;missing context A",
   343  			start: []github.Status{
   344  				makeStatus("unrelated context", "success", "description 2", "url 2"),
   345  				makeStatus("also not related", "error", "description 4", "url 4"),
   346  			},
   347  			expectedDiffs: []github.Status{},
   348  		},
   349  	}
   350  
   351  	m := *RetireMode(contextA, "", "")
   352  	for _, test := range tests {
   353  		diff := m.processStatuses(&github.CombinedStatus{Statuses: test.start})
   354  		if err := compareDiffs(diff, test.expectedDiffs); err != nil {
   355  			t.Errorf("RetireMode(NoReplace) test '%s' %v\n", test.name, err)
   356  		}
   357  	}
   358  }
   359  
   360  // makeStatus returns a new Status struct with the specified fields.
   361  // targetURL=="" means TargetURL==nil
   362  func makeStatus(context, state, description, targetURL string) github.Status {
   363  	var url string
   364  	if targetURL != "" {
   365  		url = targetURL
   366  	}
   367  	return github.Status{
   368  		Context:     context,
   369  		State:       state,
   370  		Description: description,
   371  		TargetURL:   url,
   372  	}
   373  }
   374  
   375  type refID struct {
   376  	org, repo, ref string
   377  }
   378  
   379  type fakeGitHubClient struct {
   380  	statusesRetrieved map[refID]interface{}
   381  }
   382  
   383  func (c *fakeGitHubClient) GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error) {
   384  	c.statusesRetrieved[refID{org: org, repo: repo, ref: ref}] = nil
   385  	return nil, errors.New("return error to stop execution early")
   386  }
   387  
   388  func (c *fakeGitHubClient) CreateStatus(org, repo, SHA string, s github.Status) error {
   389  	return nil
   390  }
   391  
   392  func (c *fakeGitHubClient) GetPullRequests(org, repo string) ([]github.PullRequest, error) {
   393  	return []github.PullRequest{}, nil
   394  }
   395  
   396  func TestProcessPR(t *testing.T) {
   397  	var testCases = []struct {
   398  		name    string
   399  		matches bool
   400  	}{
   401  		{
   402  			name:    "branch matching filter should proceed",
   403  			matches: true,
   404  		},
   405  		{
   406  			name:    "branch not matching filter should not proceed",
   407  			matches: false,
   408  		},
   409  	}
   410  
   411  	for _, testCase := range testCases {
   412  		client := fakeGitHubClient{statusesRetrieved: map[refID]interface{}{}}
   413  		var filteredBranch string
   414  		migrator := Migrator{
   415  			org:  "org",
   416  			repo: "repo",
   417  			targetBranchFilter: func(branch string) bool {
   418  				filteredBranch = branch
   419  				return testCase.matches
   420  			},
   421  			client: &client,
   422  		}
   423  		migrator.processPR(github.PullRequest{Base: github.PullRequestBranch{Ref: "branch"}, Head: github.PullRequestBranch{SHA: "fake"}})
   424  		if filteredBranch != "branch" {
   425  			t.Errorf("%s: failed to use filter on branch", testCase.name)
   426  		}
   427  
   428  		_, retrieved := client.statusesRetrieved[refID{org: "org", repo: "repo", ref: "fake"}]
   429  		if testCase.matches && !retrieved {
   430  			t.Errorf("%s: failed to process a PR that matched", testCase.name)
   431  		}
   432  		if !testCase.matches && retrieved {
   433  			t.Errorf("%s: processed a PR that didn't match", testCase.name)
   434  		}
   435  	}
   436  }