/* Copyright 2015 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package testing import ( "encoding/json" "fmt" "testing" "time" "net/http" "net/http/httptest" "net/url" "github.com/golang/glog" "github.com/google/go-github/github" ) var ( _ = glog.Errorf ) func stringPtr(val string) *string { return &val } func intPtr(val int) *int { return &val } func boolPtr(val bool) *bool { return &val } func timePtr(val time.Time) *time.Time { return &val } // Comment is a helper to create a valid-ish comment for testing func Comment(id int, login string, createdAt time.Time, body string) *github.IssueComment { return &github.IssueComment{ ID: &id, Body: &body, CreatedAt: &createdAt, User: &github.User{ Login: &login, }, } } // PullRequest returns a filled out github.PullRequest func PullRequest(user string, merged, mergeDetermined, mergeable bool) *github.PullRequest { pr := &github.PullRequest{ Title: stringPtr("My PR title"), Number: intPtr(1), HTMLURL: stringPtr("PR URL"), Head: &github.PullRequestBranch{ SHA: stringPtr("mysha"), }, User: &github.User{ Login: stringPtr(user), AvatarURL: stringPtr("MyAvatarURL"), }, Merged: boolPtr(merged), Base: &github.PullRequestBranch{ Ref: stringPtr("master"), }, } if mergeDetermined { pr.Mergeable = boolPtr(mergeable) } return pr } // Issue returns a filled out github.Issue func Issue(user string, number int, labels []string, isPR bool) *github.Issue { issue := &github.Issue{ Title: stringPtr("My issue title"), Number: intPtr(number), HTMLURL: stringPtr("Issue URL"), User: &github.User{ Login: stringPtr(user), AvatarURL: stringPtr("MyAvatarURL"), }, } if isPR { issue.PullRequestLinks = &github.PullRequestLinks{} } issue.Labels = StringsToLabels(labels) return issue } // StringsToLabels converts a slice of label names to a slice of // github.Label instances in non-determinastic order. func StringsToLabels(labelNames []string) []github.Label { // putting it in a map means ordering is non-deterministic lmap := map[int]github.Label{} for i, label := range labelNames { l := github.Label{ Name: stringPtr(label), } lmap[i] = l } labels := []github.Label{} for _, l := range lmap { labels = append(labels, l) } return labels } // MultiIssueEvents packages up events for when you have multiple issues in the // test server. func MultiIssueEvents(issueToEvents map[int][]LabelTime, eventName string) (out []*github.IssueEvent) { for issueNum, events := range issueToEvents { for _, l := range events { out = append(out, &github.IssueEvent{ Issue: &github.Issue{Number: intPtr(issueNum)}, Event: stringPtr(eventName), Label: &github.Label{ Name: stringPtr(l.Label), }, CreatedAt: timePtr(time.Unix(l.Time, 0)), Actor: &github.User{ Login: stringPtr(l.User), }, }) } } return out } // LabelTime is a struct which can be used to call Events() // It expresses what label the event should be about and what time // the event took place. type LabelTime struct { User string Label string Time int64 } // Events returns a slice of github.IssueEvent where the specified labels were // applied at the specified times func Events(labels []LabelTime) []*github.IssueEvent { // putting it in a map means ordering is non-deterministic eMap := map[int]*github.IssueEvent{} for i, l := range labels { event := &github.IssueEvent{ Event: stringPtr("labeled"), Label: &github.Label{ Name: stringPtr(l.Label), }, CreatedAt: timePtr(time.Unix(l.Time, 0)), Actor: &github.User{ Login: stringPtr(l.User), }, } eMap[i] = event } out := []*github.IssueEvent{} for _, e := range eMap { out = append(out, e) } return out } // Commit returns a filled out github.Commit which happened at time.Unix(t, 0) func Commit(sha string, t int64) *github.Commit { return &github.Commit{ SHA: stringPtr(sha), Committer: &github.CommitAuthor{ Date: timePtr(time.Unix(t, 0)), }, } } // IssueComment returns a filled out github.IssueComment which happened at time.Unix(t, 0). func IssueComment(id int, body string, user string, createAt int64) *github.IssueComment { return &github.IssueComment{ ID: intPtr(id), Body: stringPtr(body), User: &github.User{ Login: stringPtr(user), }, CreatedAt: timePtr(time.Unix(createAt, 0)), } } // Commits returns an array of github.RepositoryCommits. The first commit // will have happened at time `time`, the next commit `time + 1`, etc func Commits(num int, time int64) []*github.RepositoryCommit { // putting it in a map means ordering is non-deterministic cMap := map[int]*github.RepositoryCommit{} for i := 0; i < num; i++ { sha := fmt.Sprintf("mysha%d", i) t := time + int64(i) commit := &github.RepositoryCommit{ SHA: stringPtr(sha), Commit: Commit(sha, t), } cMap[i] = commit } out := []*github.RepositoryCommit{} for _, c := range cMap { out = append(out, c) } return out } func updateStatusState(status *github.CombinedStatus) *github.CombinedStatus { prioMap := map[string]int{ "pending": 4, "error": 3, "failure": 2, "success": 1, "": 0, } backMap := map[int]string{ 4: "pending", 3: "error", 2: "failure", 1: "success", 0: "", } sint := 1 for _, s := range status.Statuses { newSint := prioMap[*s.State] if newSint > sint { sint = newSint } } status.State = stringPtr(backMap[sint]) return status } func fillMap(sMap map[int]github.RepoStatus, contexts []string, state string) { for _, context := range contexts { s := github.RepoStatus{ Context: stringPtr(context), State: stringPtr(state), UpdatedAt: timePtr(time.Unix(0, 0)), CreatedAt: timePtr(time.Unix(0, 0)), } sMap[len(sMap)] = s } } // Status returns a github.CombinedStatus func Status(sha string, success []string, fail []string, pending []string, errored []string) *github.CombinedStatus { // putting it in a map means ordering is non-deterministic sMap := map[int]github.RepoStatus{} fillMap(sMap, success, "success") fillMap(sMap, fail, "failure") fillMap(sMap, pending, "pending") fillMap(sMap, errored, "error") out := &github.CombinedStatus{ SHA: stringPtr(sha), } for _, s := range sMap { out.Statuses = append(out.Statuses, s) } return updateStatusState(out) } // ServeIssue is a helper to load additional issues into the test server func ServeIssue(t *testing.T, mux *http.ServeMux, issue *github.Issue) { issueNum := *issue.Number path := fmt.Sprintf("/repos/o/r/issues/%d", issueNum) setMux(t, mux, path, issue) } func setMux(t *testing.T, mux *http.ServeMux, path string, thing interface{}) { mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { var data []byte var err error switch thing := thing.(type) { default: t.Errorf("Unexpected object type in SetMux: %v", thing) case *github.Issue: data, err = json.Marshal(thing) case *github.PullRequest: data, err = json.Marshal(thing) case []*github.IssueEvent: data, err = json.Marshal(thing) case []*github.RepositoryCommit: data, err = json.Marshal(thing) case github.RepositoryCommit: data, err = json.Marshal(thing) case *github.RepositoryCommit: data, err = json.Marshal(thing) case *github.CombinedStatus: data, err = json.Marshal(thing) case []*github.CommitFile: data, err = json.Marshal(thing) case []*github.User: data, err = json.Marshal(thing) } if err != nil { t.Errorf("%v", err) } if r.Method != "GET" && r.Method != "PATCH" { t.Errorf("Unexpected method: expected: GET got: %s", r.Method) } w.WriteHeader(http.StatusOK) w.Write(data) }) } func splitEventsByIssueNumber(defaultNumber int, events []*github.IssueEvent) map[int][]*github.IssueEvent { // The defaultNumber nonsense is to support tests that were assuming only one issue. out := map[int][]*github.IssueEvent{} for _, e := range events { n := defaultNumber if e.Issue != nil && e.Issue.Number != nil { n = *e.Issue.Number } out[n] = append(out[n], e) } return out } // InitServer will return a github.Client which will talk to httptest.Server, // to retrieve information from the http.ServeMux. If an issue, pr, events, or // commits are supplied it will repond with those on o/r/ func InitServer(t *testing.T, issue *github.Issue, pr *github.PullRequest, events []*github.IssueEvent, commits []*github.RepositoryCommit, status *github.CombinedStatus, masterCommit *github.RepositoryCommit, files []*github.CommitFile) (*github.Client, *httptest.Server, *http.ServeMux) { // test server mux := http.NewServeMux() server := httptest.NewServer(mux) // github client configured to use test server client := github.NewClient(nil) url, _ := url.Parse(server.URL) client.BaseURL = url client.UploadURL = url issueNum := 1 if issue != nil { issueNum = *issue.Number } else if pr != nil { issueNum = *pr.Number } sha := "mysha" if pr != nil { sha = *pr.Head.SHA } if issue != nil { path := fmt.Sprintf("/repos/o/r/issues/%d", issueNum) setMux(t, mux, path, issue) } if pr != nil { path := fmt.Sprintf("/repos/o/r/pulls/%d", issueNum) setMux(t, mux, path, pr) } if events != nil { for issueNum, events := range splitEventsByIssueNumber(issueNum, events) { path := fmt.Sprintf("/repos/o/r/issues/%d/events", issueNum) setMux(t, mux, path, events) } } if commits != nil { path := fmt.Sprintf("/repos/o/r/pulls/%d/commits", issueNum) setMux(t, mux, path, commits) for _, c := range commits { path := fmt.Sprintf("/repos/o/r/commits/%s", *c.SHA) setMux(t, mux, path, c) } } if masterCommit != nil { path := "/repos/o/r/commits/master" setMux(t, mux, path, masterCommit) } if files != nil { path := fmt.Sprintf("/repos/o/r/pulls/%d/files", issueNum) setMux(t, mux, path, files) } if status != nil { path := fmt.Sprintf("/repos/o/r/commits/%s/status", sha) setMux(t, mux, path, status) } path := "/repos/o/r/collaborators" setMux(t, mux, path, []github.User{}) return client, server, mux }