github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/milestone-maintainer_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 mungers
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net/http"
    23  	"reflect"
    24  	"strings"
    25  	"testing"
    26  	"time"
    27  
    28  	"k8s.io/apimachinery/pkg/util/sets"
    29  	"k8s.io/test-infra/mungegithub/github"
    30  	github_test "k8s.io/test-infra/mungegithub/github/testing"
    31  	c "k8s.io/test-infra/mungegithub/mungers/matchers/comment"
    32  
    33  	githubapi "github.com/google/go-github/github"
    34  )
    35  
    36  const milestoneTestBotName = "test-bot"
    37  
    38  // TestMilestoneMaintainer validates that notification state can be
    39  // determined and applied to an issue.  Comprehensive testing is left
    40  // to TestNotificationState.
    41  //
    42  // TODO(marun) Enable testing of comment deletion
    43  func TestMilestoneMaintainer(t *testing.T) {
    44  	activeMilestone := "v1.10"
    45  	milestone := &githubapi.Milestone{Title: &activeMilestone, Number: intPtr(1)}
    46  	m := MilestoneMaintainer{
    47  		milestoneModeMap:    map[string]string{activeMilestone: milestoneModeDev},
    48  		approvalGracePeriod: 72 * time.Hour,
    49  		labelGracePeriod:    72 * time.Hour,
    50  		warningInterval:     24 * time.Hour,
    51  	}
    52  
    53  	issue := github_test.Issue("user", 1, []string{"kind/bug", "sig/foo", "priority/important-soon"}, false)
    54  	issue.Milestone = milestone
    55  
    56  	config := &github.Config{Org: "o", Project: "r"}
    57  	client, server, mux := github_test.InitServer(t, issue, nil, nil, nil, nil, nil, nil)
    58  	config.SetClient(client)
    59  
    60  	path := fmt.Sprintf("/repos/%s/%s/issues/%d", config.Org, config.Project, *issue.Number)
    61  
    62  	mux.HandleFunc(fmt.Sprintf("%s/labels", path), func(w http.ResponseWriter, r *http.Request) {
    63  		w.WriteHeader(http.StatusOK)
    64  		out := []githubapi.Label{{}}
    65  		data, err := json.Marshal(out)
    66  		if err != nil {
    67  			t.Errorf("Unexpected error: %v", err)
    68  		}
    69  		w.Write(data)
    70  	})
    71  
    72  	var comments []githubapi.IssueComment
    73  	mux.HandleFunc(fmt.Sprintf("%s/comments", path), func(w http.ResponseWriter, r *http.Request) {
    74  		if r.Method == "POST" {
    75  			c := new(githubapi.IssueComment)
    76  			json.NewDecoder(r.Body).Decode(c)
    77  			comments = append(comments, *c)
    78  			w.WriteHeader(http.StatusOK)
    79  			data, err := json.Marshal(githubapi.IssueComment{})
    80  			if err != nil {
    81  				t.Errorf("Unexpected error: %v", err)
    82  			}
    83  			w.Write(data)
    84  			return
    85  		}
    86  		if r.Method == "GET" {
    87  			w.WriteHeader(http.StatusOK)
    88  			data, err := json.Marshal([]githubapi.IssueComment{})
    89  			if err != nil {
    90  				t.Errorf("Unexpected error: %v", err)
    91  			}
    92  			w.Write(data)
    93  			return
    94  		}
    95  		t.Fatalf("Unexpected method: %s", r.Method)
    96  	})
    97  
    98  	obj, err := config.GetObject(*issue.Number)
    99  	if err != nil {
   100  		t.Fatal(err)
   101  	}
   102  
   103  	m.Munge(obj)
   104  
   105  	expectedLabel := milestoneNeedsApprovalLabel
   106  	if !obj.HasLabel(expectedLabel) {
   107  		t.Fatalf("Issue labels do not include '%s'", expectedLabel)
   108  	}
   109  
   110  	if len(comments) != 1 {
   111  		t.Fatalf("Expected comment count of %d, got %d", 1, len(comments))
   112  	}
   113  
   114  	expectedBody := `[MILESTONENOTIFIER] Milestone Issue **Needs Approval**
   115  
   116  @user @kubernetes/sig-foo-misc
   117  
   118  
   119  **Action required**: This issue must have the ` + "`status/approved-for-milestone`" + ` label applied by a SIG maintainer. If the label is not applied within 3 days, the issue will be moved out of the v1.10 milestone.
   120  <details>
   121  <summary>Issue Labels</summary>
   122  
   123  - ` + "`sig/foo`" + `: Issue will be escalated to these SIGs if needed.
   124  - ` + "`priority/important-soon`" + `: Escalate to the issue owners and SIG owner; move out of milestone after several unsuccessful escalation attempts.
   125  - ` + "`kind/bug`" + `: Fixes a bug discovered during the current release.
   126  </details>
   127  <details>
   128  <summary>Help</summary>
   129  <ul>
   130   <li><a href="https://git.k8s.io/sig-release/ephemera/issues.md">Additional instructions</a></li>
   131   <li><a href="https://go.k8s.io/bot-commands">Commands for setting labels</a></li>
   132  </ul>
   133  </details>`
   134  	if comments[0].Body == nil || expectedBody != *comments[0].Body {
   135  		t.Fatalf("Expected Body:\n\n%s\n\nGot:\n\n%s", expectedBody, *comments[0].Body)
   136  	}
   137  
   138  	server.Close()
   139  }
   140  
   141  // TestNewIssueChangeConfig validates the creation of an IssueChange
   142  // for a given issue state.
   143  func TestNewIssueChangeConfig(t *testing.T) {
   144  	const incompleteLabels = `
   145  _**kind**_: Must specify exactly one of ` + "`kind/bug`, `kind/cleanup` or `kind/feature`." + `
   146  _**sig owner**_: Must specify at least one label prefixed with ` + "`sig/`." + `
   147  `
   148  	const blockerCompleteLabels = `
   149  <summary>Issue Labels</summary>
   150  
   151  - ` + "`sig/foo`: Issue will be escalated to these SIGs if needed." + `
   152  - ` + "`priority/critical-urgent`: Never automatically move issue out of a release milestone; continually escalate to contributor and SIG through all available channels." + `
   153  - ` + "`kind/bug`: Fixes a bug discovered during the current release." + `
   154  </details>
   155  `
   156  
   157  	const nonBlockerCompleteLabels = `
   158  <summary>Issue Labels</summary>
   159  
   160  - ` + "`sig/foo`: Issue will be escalated to these SIGs if needed." + `
   161  - ` + "`priority/important-soon`: Escalate to the issue owners and SIG owner; move out of milestone after several unsuccessful escalation attempts." + `
   162  - ` + "`kind/bug`: Fixes a bug discovered during the current release." + `
   163  </details>
   164  `
   165  
   166  	milestoneString := "v1.8"
   167  
   168  	munger := &MilestoneMaintainer{
   169  		botName:             milestoneTestBotName,
   170  		labelGracePeriod:    3 * day,
   171  		approvalGracePeriod: 7 * day,
   172  		warningInterval:     day,
   173  		slushUpdateInterval: 3 * day,
   174  		freezeDate:          "the time heck freezes over",
   175  	}
   176  
   177  	createdNow := time.Now()
   178  	createdPastLabelGracePeriod := createdNow.Add(-(munger.labelGracePeriod + time.Hour))
   179  	createdPastApprovalGracePeriod := createdNow.Add(-(munger.approvalGracePeriod + time.Hour))
   180  	createdPastSlushUpdateInterval := createdNow.Add(-(munger.slushUpdateInterval + time.Hour))
   181  
   182  	tests := map[string]struct {
   183  		// The mode of the munger
   184  		mode string
   185  		// Labels to add to the test issue
   186  		labels []string
   187  		// Whether the test issues milestone labels should be complete
   188  		labelsComplete bool
   189  		// Whether the test issue should be a blocker
   190  		isBlocker bool
   191  		// Whether the test issue should be approved for the milestone
   192  		isApproved bool
   193  		// Events to add to the test issue
   194  		events []*githubapi.IssueEvent
   195  		// Comments to add to the test issue
   196  		comments []*githubapi.IssueComment
   197  		// Sections expected to be enabled
   198  		expectedSections sets.String
   199  		// Expected milestone state
   200  		expectedState milestoneState
   201  		// Expected message body
   202  		expectedBody string
   203  	}{
   204  		"Incomplete labels within grace period": {
   205  			expectedSections: sets.NewString("warnIncompleteLabels"),
   206  			expectedState:    milestoneNeedsLabeling,
   207  			expectedBody: `
   208  **Action required**: This issue requires label changes. If the required changes are not made within 3 days, the issue will be moved out of the v1.8 milestone.
   209  ` + incompleteLabels,
   210  		},
   211  		"Incomplete labels outside of grace period": {
   212  			labels:           []string{milestoneLabelsIncompleteLabel},
   213  			events:           milestoneLabelEvents(milestoneLabelsIncompleteLabel, createdPastLabelGracePeriod),
   214  			expectedSections: sets.NewString("removeIncompleteLabels"),
   215  			expectedState:    milestoneNeedsRemoval,
   216  			expectedBody: `
   217  **Important**: This issue was missing labels required for the v1.8 milestone for more than 3 days:
   218  
   219  _**kind**_: Must specify exactly one of ` + "`kind/bug`, `kind/cleanup` or `kind/feature`." + `
   220  _**sig owner**_: Must specify at least one label prefixed with ` + "`sig/`.",
   221  		},
   222  		"Incomplete labels outside of grace period, blocker": {
   223  			labels:           []string{milestoneLabelsIncompleteLabel},
   224  			isBlocker:        true,
   225  			events:           milestoneLabelEvents(milestoneLabelsIncompleteLabel, createdPastLabelGracePeriod),
   226  			expectedSections: sets.NewString("warnIncompleteLabels"),
   227  			expectedState:    milestoneNeedsLabeling,
   228  			expectedBody: `
   229  **Action required**: This issue requires label changes.
   230  
   231  _**kind**_: Must specify exactly one of ` + "`kind/bug`, `kind/cleanup` or `kind/feature`." + `
   232  _**sig owner**_: Must specify at least one label prefixed with ` + "`sig/`." + `
   233  `,
   234  		},
   235  		"Complete labels, not approved, blocker": {
   236  			labelsComplete:   true,
   237  			isBlocker:        true,
   238  			expectedSections: sets.NewString("summarizeLabels", "warnUnapproved"),
   239  			expectedState:    milestoneNeedsApproval,
   240  			expectedBody: `
   241  **Action required**: This issue must have the ` + "`status/approved-for-milestone`" + ` label applied by a SIG maintainer.
   242  <details>` + blockerCompleteLabels,
   243  		},
   244  		"Complete labels, not approved, non-blocker, within grace period": {
   245  			labelsComplete:   true,
   246  			expectedSections: sets.NewString("summarizeLabels", "warnUnapproved"),
   247  			expectedState:    milestoneNeedsApproval,
   248  			expectedBody: `
   249  **Action required**: This issue must have the ` + "`status/approved-for-milestone`" + ` label applied by a SIG maintainer. If the label is not applied within 7 days, the issue will be moved out of the v1.8 milestone.
   250  <details>` + nonBlockerCompleteLabels,
   251  		},
   252  		"Complete labels, not approved, non-blocker, outside of grace period": {
   253  			labels:           []string{milestoneNeedsApprovalLabel},
   254  			labelsComplete:   true,
   255  			events:           milestoneLabelEvents(milestoneNeedsApprovalLabel, createdPastApprovalGracePeriod),
   256  			expectedSections: sets.NewString("summarizeLabels", "removeUnapproved"),
   257  			expectedState:    milestoneNeedsRemoval,
   258  			expectedBody:     "**Important**: This issue was missing the `status/approved-for-milestone` label for more than 7 days.",
   259  		},
   260  		"dev - Complete labels and approved": {
   261  			labelsComplete:   true,
   262  			isApproved:       true,
   263  			expectedSections: sets.NewString("summarizeLabels"),
   264  			expectedState:    milestoneCurrent,
   265  			expectedBody:     "<details open>" + nonBlockerCompleteLabels,
   266  		},
   267  		"slush - Complete labels, approved, non-blocker, missing in-progress": {
   268  			mode:             milestoneModeSlush,
   269  			labelsComplete:   true,
   270  			isApproved:       true,
   271  			expectedSections: sets.NewString("summarizeLabels", "warnMissingInProgress", "warnNonBlockerRemoval"),
   272  			expectedState:    milestoneNeedsAttention,
   273  			expectedBody: `
   274  **Action required**: During code slush, issues in the milestone should be in progress.
   275  If this issue is not being actively worked on, please remove it from the milestone.
   276  If it is being worked on, please add the ` + "`status/in-progress`" + ` label so it can be tracked with other in-flight issues.
   277  
   278  **Note**: If this issue is not resolved or labeled as ` + "`priority/critical-urgent`" + ` by the time heck freezes over it will be moved out of the v1.8 milestone.
   279  <details>` + nonBlockerCompleteLabels,
   280  		},
   281  		"slush - Complete labels, approved, non-blocker": {
   282  			mode:             milestoneModeSlush,
   283  			labels:           []string{"status/in-progress"},
   284  			labelsComplete:   true,
   285  			isApproved:       true,
   286  			expectedSections: sets.NewString("summarizeLabels", "warnNonBlockerRemoval"),
   287  			expectedState:    milestoneCurrent,
   288  			expectedBody: `
   289  **Note**: If this issue is not resolved or labeled as ` + "`priority/critical-urgent`" + ` by the time heck freezes over it will be moved out of the v1.8 milestone.
   290  <details open>` + nonBlockerCompleteLabels,
   291  		},
   292  		"slush - Complete labels, approved, blocker, missing in-progress, update not due": {
   293  			mode:             milestoneModeSlush,
   294  			labelsComplete:   true,
   295  			isApproved:       true,
   296  			isBlocker:        true,
   297  			events:           milestoneLabelEvents(statusApprovedLabel, createdNow),
   298  			comments:         milestoneIssueComments(createdNow),
   299  			expectedSections: sets.NewString("summarizeLabels", "warnMissingInProgress", "warnUpdateInterval"),
   300  			expectedState:    milestoneNeedsAttention,
   301  			expectedBody: `
   302  **Action required**: During code slush, issues in the milestone should be in progress.
   303  If this issue is not being actively worked on, please remove it from the milestone.
   304  If it is being worked on, please add the ` + "`status/in-progress`" + ` label so it can be tracked with other in-flight issues.
   305  
   306  **Note**: This issue is marked as ` + "`priority/critical-urgent`" + `, and must be updated every 3 days during code slush.
   307  
   308  Example update:
   309  
   310  ` + "```" + `
   311  ACK.  In progress
   312  ETA: DD/MM/YYYY
   313  Risks: Complicated fix required
   314  ` + "```" + `
   315  <details>` + blockerCompleteLabels,
   316  		},
   317  		"slush - Complete labels, approved, blocker, update not due": {
   318  			mode:             milestoneModeSlush,
   319  			labels:           []string{"status/in-progress"},
   320  			labelsComplete:   true,
   321  			isApproved:       true,
   322  			isBlocker:        true,
   323  			events:           milestoneLabelEvents(statusApprovedLabel, createdNow),
   324  			comments:         milestoneIssueComments(createdNow),
   325  			expectedSections: sets.NewString("summarizeLabels", "warnUpdateInterval"),
   326  			expectedState:    milestoneCurrent,
   327  			expectedBody: `
   328  **Note**: This issue is marked as ` + "`priority/critical-urgent`" + `, and must be updated every 3 days during code slush.
   329  
   330  Example update:
   331  
   332  ` + "```" + `
   333  ACK.  In progress
   334  ETA: DD/MM/YYYY
   335  Risks: Complicated fix required
   336  ` + "```" + `
   337  <details open>` + blockerCompleteLabels,
   338  		},
   339  		"slush - Complete labels, approved, blocker, update due": {
   340  			mode:             milestoneModeSlush,
   341  			labels:           []string{"status/in-progress"},
   342  			labelsComplete:   true,
   343  			isApproved:       true,
   344  			isBlocker:        true,
   345  			events:           milestoneLabelEvents(statusApprovedLabel, createdNow),
   346  			comments:         milestoneIssueComments(createdPastSlushUpdateInterval),
   347  			expectedSections: sets.NewString("summarizeLabels", "warnUpdateInterval", "warnUpdateRequired"),
   348  			expectedState:    milestoneNeedsAttention,
   349  			expectedBody: `
   350  **Action Required**: This issue has not been updated since ` + createdPastSlushUpdateInterval.Format("Jan 2") + `. Please provide an update.
   351  
   352  **Note**: This issue is marked as ` + "`priority/critical-urgent`" + `, and must be updated every 3 days during code slush.
   353  
   354  Example update:
   355  
   356  ` + "```" + `
   357  ACK.  In progress
   358  ETA: DD/MM/YYYY
   359  Risks: Complicated fix required
   360  ` + "```" + `
   361  <details>` + blockerCompleteLabels,
   362  		},
   363  		"freeze - Complete labels, approved, non-blocker": {
   364  			mode:             milestoneModeFreeze,
   365  			labelsComplete:   true,
   366  			isApproved:       true,
   367  			expectedSections: sets.NewString("summarizeLabels", "removeNonBlocker"),
   368  			expectedState:    milestoneNeedsRemoval,
   369  			expectedBody:     "**Important**: Code freeze is in effect and only issues with `priority/critical-urgent` may remain in the v1.8 milestone.",
   370  		},
   371  	}
   372  	for testName, test := range tests {
   373  		t.Run(testName, func(t *testing.T) {
   374  			mode := milestoneModeDev
   375  			if len(test.mode) > 0 {
   376  				mode = test.mode
   377  			}
   378  			munger.milestoneModeMap = map[string]string{milestoneString: mode}
   379  
   380  			labels := test.labels
   381  			if test.isBlocker {
   382  				labels = append(labels, blockerLabel)
   383  			} else {
   384  				labels = append(labels, "priority/important-soon")
   385  			}
   386  			if test.labelsComplete {
   387  				labels = append(labels, "kind/bug")
   388  				labels = append(labels, "sig/foo")
   389  			}
   390  			if test.isApproved {
   391  				labels = append(labels, statusApprovedLabel)
   392  			}
   393  
   394  			issue := github_test.Issue("user", 1, labels, false)
   395  			// Ensure issue was created before any comments or events
   396  			createdLongAgo := createdNow.Add(-28 * day)
   397  			issue.CreatedAt = &createdLongAgo
   398  			milestone := &githubapi.Milestone{Title: stringPtr(milestoneString), Number: intPtr(1)}
   399  			issue.Milestone = milestone
   400  
   401  			client, server, mux := github_test.InitServer(t, issue, nil, test.events, nil, nil, nil, nil)
   402  			defer server.Close()
   403  
   404  			config := &github.Config{Org: "o", Project: "r"}
   405  
   406  			path := fmt.Sprintf("/repos/%s/%s/issues/%d", config.Org, config.Project, *issue.Number)
   407  			mux.HandleFunc(fmt.Sprintf("%s/comments", path), func(w http.ResponseWriter, r *http.Request) {
   408  				if r.Method == "GET" {
   409  					w.WriteHeader(http.StatusOK)
   410  					data, err := json.Marshal(test.comments)
   411  					if err != nil {
   412  						t.Errorf("Unexpected error: %v", err)
   413  					}
   414  					w.Write(data)
   415  					return
   416  				}
   417  				t.Fatalf("Unexpected method: %s", r.Method)
   418  			})
   419  
   420  			config.SetClient(client)
   421  			obj, err := config.GetObject(*issue.Number)
   422  			if err != nil {
   423  				t.Fatal(err)
   424  			}
   425  
   426  			icc := munger.issueChangeConfig(obj)
   427  			if icc == nil {
   428  				t.Fatalf("%s: Expected non-nil issue change config", testName)
   429  			}
   430  
   431  			if !test.expectedSections.Equal(icc.enabledSections) {
   432  				t.Fatalf("%s: Expected sections %v, got %v", testName, test.expectedSections, icc.enabledSections)
   433  			}
   434  
   435  			if test.expectedState != icc.state {
   436  				t.Fatalf("%s: Expected state %v, got %v", testName, test.expectedState, icc.state)
   437  			}
   438  
   439  			messageBody := icc.messageBody()
   440  			if messageBody == nil {
   441  				t.Fatalf("%s: Expected non-nil message body", testName)
   442  			}
   443  			expectedBody := strings.TrimSpace(test.expectedBody)
   444  			trimmedBody := strings.TrimSpace(*messageBody)
   445  			if expectedBody != trimmedBody {
   446  				t.Fatalf("%s: Expected message body:\n\n%s\nGot:\n\n%s", testName, expectedBody, trimmedBody)
   447  			}
   448  		})
   449  	}
   450  }
   451  
   452  func milestoneTestComment(title string, context string, createdAt time.Time) *c.Comment {
   453  	n := &c.Notification{
   454  		Name:      milestoneNotifierName,
   455  		Arguments: title,
   456  		Context:   context,
   457  	}
   458  	return &c.Comment{
   459  		Body:      stringPtr(n.String()),
   460  		CreatedAt: &createdAt,
   461  	}
   462  }
   463  
   464  func milestoneLabelEvents(label string, createdAt time.Time) []*githubapi.IssueEvent {
   465  	return []*githubapi.IssueEvent{
   466  		{
   467  			Event: stringPtr("labeled"),
   468  			Label: &githubapi.Label{
   469  				Name: &label,
   470  			},
   471  			CreatedAt: &createdAt,
   472  			Actor: &githubapi.User{
   473  				Login: stringPtr(milestoneTestBotName),
   474  			},
   475  		},
   476  	}
   477  }
   478  
   479  func milestoneIssueComments(createdAt time.Time) []*githubapi.IssueComment {
   480  	return []*githubapi.IssueComment{
   481  		{
   482  			Body:      stringPtr("foo"),
   483  			UpdatedAt: &createdAt,
   484  			CreatedAt: &createdAt,
   485  			User: &githubapi.User{
   486  				Login: githubapi.String("bar"),
   487  			},
   488  		},
   489  	}
   490  }
   491  
   492  func TestNotificationIsCurrent(t *testing.T) {
   493  	createdNow := time.Now()
   494  	warningInterval := day
   495  	createdYesterday := createdNow.Add(-(warningInterval + time.Hour))
   496  
   497  	realSample := "@foo @bar @baz\n\n**Action required**: This issue requires label changes. If the required changes are not made within 6 days, the issue will be moved out of the v1.8 milestone.\n\n_**kind**_: Must specify at most one of [`kind/bug`, `kind/cleanup`, `kind/feature`].\n_**priority**_: Must specify at most one of [`priority/critical-urgent`, `priority/important-longterm`, `priority/important-soon`].\n_**sig owner**_: Must specify at least one label prefixed with `sig/`.\n\n<details>\nAdditional instructions available <a href=\"https://git.k8s.io/sig-release/ephemera/issues.md\">here</a>\n</details>"
   498  
   499  	tests := map[string]struct {
   500  		message            string
   501  		newMessage         string
   502  		createdAt          time.Time
   503  		nilCommentInterval bool
   504  		expectedIsCurrent  bool
   505  	}{
   506  		"Not current if no notification exists": {},
   507  		"Not current if the message is different": {
   508  			message:    "foo",
   509  			newMessage: "bar",
   510  		},
   511  		"Not current if the warning interval has elapsed": {
   512  			message:    "foo",
   513  			newMessage: "foo",
   514  			createdAt:  createdYesterday,
   515  		},
   516  		"Not current if the message is different and the comment interval is nil": {
   517  			message:            "foo",
   518  			newMessage:         "bar",
   519  			nilCommentInterval: true,
   520  		},
   521  		"Notification is current, real sample": {
   522  			message:           realSample,
   523  			newMessage:        realSample,
   524  			createdAt:         createdNow,
   525  			expectedIsCurrent: true,
   526  		},
   527  	}
   528  	for testName, test := range tests {
   529  		t.Run(testName, func(t *testing.T) {
   530  			var oldComment *c.Comment
   531  			if len(test.message) > 0 {
   532  				oldComment = milestoneTestComment("foo", test.message, test.createdAt)
   533  			}
   534  			newComment := milestoneTestComment("foo", test.newMessage, createdNow)
   535  			notification := c.ParseNotification(newComment)
   536  			var commentInterval *time.Duration
   537  			if !test.nilCommentInterval {
   538  				commentInterval = &warningInterval
   539  			}
   540  			isCurrent := notificationIsCurrent(notification, oldComment, commentInterval)
   541  			if test.expectedIsCurrent != isCurrent {
   542  				t.Logf("notification %#v\n", notification)
   543  				t.Fatalf("%s: expected warningIsCurrent to be %t, but got %t", testName, test.expectedIsCurrent, isCurrent)
   544  			}
   545  		})
   546  	}
   547  }
   548  
   549  func TestIgnoreObject(t *testing.T) {
   550  	tests := map[string]struct {
   551  		isClosed        bool
   552  		milestone       string
   553  		activeMilestone string
   554  		expectedIgnore  bool
   555  	}{
   556  		"Ignore closed issue": {
   557  			isClosed:       true,
   558  			expectedIgnore: true,
   559  		},
   560  		"Do not ignore open issue": {},
   561  	}
   562  	for testName, test := range tests {
   563  		t.Run(testName, func(t *testing.T) {
   564  			issue := github_test.Issue("user", 1, nil, false)
   565  			issue.Milestone = &githubapi.Milestone{Title: stringPtr(test.milestone), Number: intPtr(1)}
   566  			if test.isClosed {
   567  				issue.State = stringPtr("closed")
   568  			}
   569  			obj := &github.MungeObject{Issue: issue}
   570  
   571  			ignore := ignoreObject(obj)
   572  
   573  			if ignore != test.expectedIgnore {
   574  				t.Fatalf("%s: Expected ignore to be %t, got %t", testName, test.expectedIgnore, ignore)
   575  			}
   576  		})
   577  
   578  	}
   579  }
   580  
   581  func TestUniqueLabelName(t *testing.T) {
   582  	labelMap := map[string]string{
   583  		"foo": "",
   584  		"bar": "",
   585  	}
   586  	tests := map[string]struct {
   587  		labelNames    []string
   588  		expectedLabel string
   589  		expectedErr   bool
   590  	}{
   591  		"Unmatched label set returns empty string": {},
   592  		"Single label match returned": {
   593  			labelNames:    []string{"foo"},
   594  			expectedLabel: "foo",
   595  		},
   596  		"Multiple label matches returns error": {
   597  			labelNames:  []string{"foo", "bar"},
   598  			expectedErr: true,
   599  		},
   600  	}
   601  	for testName, test := range tests {
   602  		t.Run(testName, func(t *testing.T) {
   603  			labels := github_test.StringsToLabels(test.labelNames)
   604  
   605  			label, err := uniqueLabelName(labels, labelMap)
   606  
   607  			if label != test.expectedLabel {
   608  				t.Fatalf("%s: Expected label '%s', got '%s'", testName, test.expectedLabel, label)
   609  			}
   610  			if test.expectedErr && err == nil {
   611  				t.Fatalf("%s: Err expected but did not occur", testName)
   612  			}
   613  			if !test.expectedErr && err != nil {
   614  				t.Fatalf("%s: Unexpected error occurred", testName)
   615  			}
   616  		})
   617  	}
   618  }
   619  
   620  func TestSigLabelNames(t *testing.T) {
   621  	labels := github_test.StringsToLabels([]string{"sig/foo", "sig/bar", "baz"})
   622  	labelNames := sigLabelNames(labels)
   623  	// Expect labels without sig/ prefix to be filtered out
   624  	expectedLabelNames := []string{"sig/foo", "sig/bar"}
   625  	if len(expectedLabelNames) != len(labelNames) {
   626  		t.Fatalf("Wrong number of labels. Got %v, wanted %v.", labelNames, expectedLabelNames)
   627  	}
   628  	for _, ln1 := range expectedLabelNames {
   629  		var found bool
   630  		for _, ln2 := range labelNames {
   631  			if ln1 == ln2 {
   632  				found = true
   633  			}
   634  		}
   635  		if !found {
   636  			t.Errorf("Label %s not found in %v", ln1, labelNames)
   637  		}
   638  	}
   639  }
   640  
   641  func TestParseMilestoneModes(t *testing.T) {
   642  	tests := map[string]struct {
   643  		input       string
   644  		output      map[string]string
   645  		errExpected bool
   646  	}{
   647  		"Empty string": {
   648  			errExpected: true,
   649  		},
   650  		"Too many = separators": {
   651  			input:       "v1.8==dev",
   652  			errExpected: true,
   653  		},
   654  		"Too many , separators": {
   655  			input:       "v1.8=dev,,v1.9=dev",
   656  			errExpected: true,
   657  		},
   658  		"Missing milestone": {
   659  			input:       "=dev",
   660  			errExpected: true,
   661  		},
   662  		"Missing mode": {
   663  			input:       "v1.8=",
   664  			errExpected: true,
   665  		},
   666  		"Invalid mode": {
   667  			input:       "v1.8=foo",
   668  			errExpected: true,
   669  		},
   670  		"Duplicated milestone": {
   671  			input:       "v1.8=dev,v1.8=slush",
   672  			errExpected: true,
   673  		},
   674  		"Single milestone": {
   675  			input:  "v1.8=dev",
   676  			output: map[string]string{"v1.8": "dev"},
   677  		},
   678  		"Multiple milestones": {
   679  			input:  "v1.8=dev,v1.9=slush",
   680  			output: map[string]string{"v1.8": "dev", "v1.9": "slush"},
   681  		},
   682  	}
   683  	for testName, test := range tests {
   684  		output, err := parseMilestoneModes(test.input)
   685  		if test.errExpected && err == nil {
   686  			t.Fatalf("%s: Expected an error to have occurred", testName)
   687  		}
   688  		if !test.errExpected && err != nil {
   689  			t.Fatalf("%s: Expected no error but got: %v", testName, err)
   690  		}
   691  
   692  		if !reflect.DeepEqual(test.output, output) {
   693  			t.Fatalf("%s: Expected output %v, got %v", testName, test.output, output)
   694  		}
   695  	}
   696  }