From 3fd8236ecc212038bfbdac2118aa92e53e4c8a62 Mon Sep 17 00:00:00 2001 From: chaodaiG <45011425+chaodaiG@users.noreply.github.com> Date: Tue, 5 Mar 2019 10:20:39 -0800 Subject: [PATCH] Flaky test reporting(part #4) Github issues tracking for flaky tests (#506) * working on github issues tracking * Update tools/flaky-test-reporter/github_issue.go Co-Authored-By: chaodaiG <45011425+chaodaiG@users.noreply.github.com> * update based on PR comments --- tools/flaky-test-reporter/config.go | 13 +- tools/flaky-test-reporter/error.go | 36 ++ tools/flaky-test-reporter/ghutil/ghutil.go | 29 +- tools/flaky-test-reporter/github_issue.go | 459 +++++++++++++++++++++ tools/flaky-test-reporter/main.go | 7 +- tools/flaky-test-reporter/result.go | 50 ++- tools/flaky-test-reporter/run.go | 32 ++ 7 files changed, 616 insertions(+), 10 deletions(-) create mode 100644 tools/flaky-test-reporter/error.go create mode 100644 tools/flaky-test-reporter/github_issue.go create mode 100644 tools/flaky-test-reporter/run.go diff --git a/tools/flaky-test-reporter/config.go b/tools/flaky-test-reporter/config.go index 3cb3cee003..9e1145af1a 100644 --- a/tools/flaky-test-reporter/config.go +++ b/tools/flaky-test-reporter/config.go @@ -24,11 +24,16 @@ import ( const ( // Builds to be analyzed, this is an arbitrary number - buildsCount = 10 + buildsCount = 10 // Minimal number of results to be counted as valid results for each testcase, this is an arbitrary number - requiredCount = 8 - // Don't do anything if found more than 5% tests flaky, this is an arbitrary number - threshold = 0.05 + requiredCount = 8 + // Don't do anything if found more than 1% tests flaky, this is an arbitrary number + threshold = 0.01 + + org = "knative" + // Temporarily creating issues under "test-infra" for better management + // TODO(chaodaiG): repo for issue same as the src of the test + repoForIssue = "test-infra" ) // jobConfigs defines which job to be monitored for giving repo diff --git a/tools/flaky-test-reporter/error.go b/tools/flaky-test-reporter/error.go new file mode 100644 index 0000000000..48fdf394b1 --- /dev/null +++ b/tools/flaky-test-reporter/error.go @@ -0,0 +1,36 @@ +/* +Copyright 2019 The Knative 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. +*/ + +// error.go helps with error handling + +package main + +import ( + "fmt" + "strings" +) + +// combineErrors combines slice of errors and return a single error +func combineErrors(errs []error) error { + if nil == errs || 0 == len(errs) { + return nil + } + var errStrs []string + for _, err := range errs { + errStrs = append(errStrs, err.Error()) + } + return fmt.Errorf(strings.Join(errStrs, "\n")) +} diff --git a/tools/flaky-test-reporter/ghutil/ghutil.go b/tools/flaky-test-reporter/ghutil/ghutil.go index fdf28a6471..5b0818ea21 100644 --- a/tools/flaky-test-reporter/ghutil/ghutil.go +++ b/tools/flaky-test-reporter/ghutil/ghutil.go @@ -38,7 +38,7 @@ const ( // IssueOpenState is the state of open github issue IssueOpenState IssueStateEnum = "open" // IssueCloseState is the state of closed github issue - IssueCloseState IssueStateEnum = "close" + IssueCloseState IssueStateEnum = "closed" // IssueAllState is the state for all, useful when querying issues IssueAllState IssueStateEnum = "all" ) @@ -80,6 +80,31 @@ func GetCurrentUser() (*github.User, error) { return res, err } +// ListRepos lists repos under org +func ListRepos(org string) ([]string, error) { + var res []string + options := &github.ListOptions{} + genericList, err := depaginate( + "listing repos", + maxRetryCount, + options, + func() ([]interface{}, *github.Response, error) { + page, resp, err := client.Repositories.List(ctx, org, nil) + var interfaceList []interface{} + if nil == err { + for _, repo := range page { + interfaceList = append(interfaceList, repo) + } + } + return interfaceList, resp, err + }, + ) + for _, repo := range genericList { + res = append(res, repo.(*github.Repository).GetName()) + } + return res, err +} + // ListIssuesByRepo lists issues within given repo, filters by labels if provided func ListIssuesByRepo(org, repo string, labels []string) ([]*github.Issue, error) { issueListOptions := github.IssueListByRepoOptions{ @@ -92,7 +117,7 @@ func ListIssuesByRepo(org, repo string, labels []string) ([]*github.Issue, error var res []*github.Issue options := &github.ListOptions{} genericList, err := depaginate( - fmt.Sprintf("listing genericList with label '%v'", labels), + fmt.Sprintf("listing issues with label '%v'", labels), maxRetryCount, options, func() ([]interface{}, *github.Response, error) { diff --git a/tools/flaky-test-reporter/github_issue.go b/tools/flaky-test-reporter/github_issue.go new file mode 100644 index 0000000000..76bf9dbd8d --- /dev/null +++ b/tools/flaky-test-reporter/github_issue.go @@ -0,0 +1,459 @@ +/* +Copyright 2019 The Knative 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 main + +import ( + "fmt" + "log" + "regexp" + "sort" + "strings" + "time" + + "github.com/google/go-github/github" + + "github.com/knative/test-infra/shared/junit" + "github.com/knative/test-infra/tools/flaky-test-reporter/ghutil" +) + +const ( + // flakyLabel is the Github issue label used for querying all flaky issues auto-generated. + flakyLabel = "auto:flaky" + testIdentifierToken = "DONT_MODIFY_TEST_IDENTIFIER" + latestStatusToken = "Latest result for this test: " + beforeHistoryToken = "------Latest History of Up To 10 runs------" + afterHistoryToken = "------End of History------" + gubernatorURL = "https://gubernator.knative.dev/build/knative-prow/logs/" + daysConsiderOld = 30 // arbitrary number of days for an issue to be considered old + maxHistoryEntries = 10 // max count of history runs to show in unicode graph + + passedUnicode = "✔" // Checkmark unicode + failedUnicode = "✖" // Cross unicode + skippedUnicode = "◻" // Open square unicode + + // collapseTemplate is for creating collapsible format in comment, so that only latest result is expanded. + // Github markdown supports collapse, strings between is the title, + // contents between

is collapsible body, body is hidden unless the title is clicked. + collapseTemplate = ` +
+ %s

+ +%s +

` // The blank line is crucial for collapsible format + + // issueBodyTemplate is a template for issue body + issueBodyTemplate = ` +\ +Test name: %s + +-------------Text below this line is for issues mapping, Please don't edit------------- +%s` +) + +var ( + // testIdentifierPattern is used for formatting test identifier, + // expect an argument of test identifier + testIdentifierPattern = fmt.Sprintf("[%[1]s]%%s[%[1]s]", testIdentifierToken) + // reTestIdentifier is regex matching pattern for capturing testname + reTestIdentifier = fmt.Sprintf(`\[%[1]s\](.*?)\[%[1]s\]`, testIdentifierToken) + + // historyPattern is for creating history section in commment, + // expect an argument of history Unicode from previous comment + historyPattern = fmt.Sprintf("\n%s%%s%s\n%s Passed\t%s Failed\t%s Skipped\n", + beforeHistoryToken, afterHistoryToken, passedUnicode, failedUnicode, skippedUnicode) + // reHistory is for identifying history from comment + reHistory = fmt.Sprintf("(?s)%s(.*?)%s", beforeHistoryToken, afterHistoryToken) + + // latestStatusPattern is for creating latest test result line in comment, + // expect an argument of test status as defined in result.go + latestStatusPattern = fmt.Sprintf(`%s%%s`, latestStatusToken) + // reLastestStatus is for identifying latest test result from comment + reLastestStatus = fmt.Sprintf(`%s([a-zA-Z]*)`, latestStatusToken) +) + +// Precompute timeConsiderOld so that the same standard used everywhere +var timeConsiderOld = time.Now().AddDate(0, 0, -daysConsiderOld) + +// currentUser is current authenticated user, used for identifying comments created by bot +var currentUser *github.User + +// flakyIssue is a wrapper of github.Issue, used for storing pre-computed information +type flakyIssue struct { + issue *github.Issue + identity *string // identity discovered by matching reTestIdentifier + comment *github.IssueComment // The first auto comment, updated for every history +} + +// createCommentForTest summarizes latest status of current test case, +// and creates text to be added to issue comment +func createCommentForTest(rd *RepoData, testFullName string) string { + ts := rd.TestStats[testFullName] + totalCount := len(ts.Passed) + len(ts.Skipped) + len(ts.Failed) + lastBuildStartTimeStr := time.Unix(*rd.LastBuildStartTime, 0).String() + content := fmt.Sprintf("%s\nLast build start time: %s\nFailed %d times out of %d runs.", + fmt.Sprintf(latestStatusPattern, ts.getTestStatus()), + lastBuildStartTimeStr, len(ts.Failed), totalCount) + if len(ts.Failed) > 0 { + content += " Failed runs: " + var buildIDContents []string + for _, buildID := range ts.Failed { + buildIDContents = append(buildIDContents, + fmt.Sprintf("[%d](%s%s/%d)", buildID, gubernatorURL, rd.Config.Name, buildID)) + } + content += strings.Join(buildIDContents, ", ") + } + return content +} + +func createHistoryUnicode(rd *RepoData, comment, testFullName string) string { + res := "" + currentUnicode := fmt.Sprintf("%s: ", time.Unix(*rd.LastBuildStartTime, 0).String()) + resultSlice := rd.getResultSliceForTest(testFullName) + for i, buildID := range rd.BuildIDs { + url := fmt.Sprintf("%s%s/%d", gubernatorURL, rd.Config.Name, buildID) + var statusUnicode string + switch resultSlice[i] { + case junit.Passed: + statusUnicode = passedUnicode + case junit.Failed: + statusUnicode = failedUnicode + default: + statusUnicode = skippedUnicode + } + currentUnicode += fmt.Sprintf(" [%s](%s)", statusUnicode, url) + } + + oldHistory := regexp.MustCompile(reHistory).FindStringSubmatch(comment) + res = fmt.Sprintf("\n%s", currentUnicode) + if len(oldHistory) >= 2 { + oldHistoryEntries := strings.Split(oldHistory[1], "\n") + if len(oldHistoryEntries) >= maxHistoryEntries { + oldHistoryEntries = oldHistoryEntries[:(maxHistoryEntries - 1)] + } + res += strings.Join(oldHistoryEntries, "\n") + } else { + res += "\n" + } + + return fmt.Sprintf(historyPattern, res) +} + +// prependComment hides old comment into a collapsible, and prepend +// new commment on top +func prependComment(oldComment, newComment string) string { + if "" != oldComment { + oldComment = fmt.Sprintf(collapseTemplate, "Click to see older results", oldComment) + } + return fmt.Sprintf("%s\n\n%s", newComment, oldComment) +} + +// updateIssue adds comments to an existing issue, close an issue if test passed both in previous day and today, +// reopens the issue if test becomes flaky while issue is closed. +func updateIssue(fi *flakyIssue, newComment string, ts *TestStat, dryrun *bool) error { + issue := fi.issue + passedLastTime := false + latestStatus := regexp.MustCompile(reLastestStatus).FindStringSubmatch(fi.comment.GetBody()) + if len(latestStatus) >= 2 { + switch latestStatus[1] { + case passedStatus: + passedLastTime = true + case flakyStatus, failedStatus, lackDataStatus: + + default: + return fmt.Errorf("invalid test status code found from issue '%s'", *issue.URL) + } + } + + // Update comment unless test passed and issue closed + if !ts.isPassed() || issue.GetState() == string(ghutil.IssueOpenState) { + if err := run( + "updating comment", + func() error { + return ghutil.EditComment(org, repoForIssue, *fi.comment.ID, prependComment(*fi.comment.Body, newComment)) + }, + dryrun); nil != err { + return fmt.Errorf("failed updating comments for issue '%s': '%v'", *issue.URL, err) + } + } + + if ts.isPassed() { // close open issue if the test passed twice consecutively + if issue.GetState() == string(ghutil.IssueOpenState) { + if passedLastTime { + if err := run( + "closing issue", + func() error { + return ghutil.CloseIssue(org, repoForIssue, *issue.Number) + }, + dryrun); nil != err { + return fmt.Errorf("failed closing issue '%s': '%v'", *issue.URL, err) + } + } + } + } else if ts.isFlaky() { // reopen closed issue if test found flaky + if issue.GetState() == string(ghutil.IssueCloseState) { + if err := run( + "reopening issue", + func() error { + return ghutil.ReopenIssue(org, repoForIssue, *issue.Number) + }, + dryrun); nil != err { + return fmt.Errorf("failed reopen issue: '%s'", *issue.URL) + } + } + } + return nil +} + +// createNewIssue creates an issue, adds flaky label and adds comment. +func createNewIssue(org, repoForIssue, title, body string, comment string, dryrun *bool) error { + var newIssue *github.Issue + if err := run( + "creating issue", + func() error { + var err error + newIssue, err = ghutil.CreateIssue(org, repoForIssue, title, body) + return err + }, + dryrun, + ); nil != err { + return fmt.Errorf("failed creating issue '%s' in repo '%s'", title, repoForIssue) + } + if err := run( + "adding comment", + func() error { + return ghutil.CreateComment(org, repoForIssue, *newIssue.Number, comment) + }, + dryrun, + ); nil != err { + return fmt.Errorf("failed adding comment to issue '%s', '%v'", *newIssue.URL, err) + } + if err := run( + "adding flaky label", + func() error { + return ghutil.AddLabelsToIssue(org, repoForIssue, *newIssue.Number, []string{flakyLabel}) + }, + dryrun, + ); nil != err { + return fmt.Errorf("failed adding '%s' label to issue '%s', '%v'", flakyLabel, *newIssue.URL, err) + } + return nil +} + +// findExistingComment identify existing comment by comment author and test identifier, +// if multiple comments were found return the earliest one. +func findExistingComment(issue *github.Issue, issueIdentity string) (*github.IssueComment, error) { + var targetComment *github.IssueComment + comments, err := ghutil.ListComments(org, repoForIssue, *issue.Number) + if nil != err { + return nil, err + } + sort.Slice(comments, func(i, j int) bool { + return nil != comments[i].CreatedAt && (nil == comments[j].CreatedAt || comments[i].CreatedAt.Before(*comments[j].CreatedAt)) + }) + + for i, comment := range comments { + if *comment.User.ID != *currentUser.ID { + continue + } + if testNameFromComment := regexp.MustCompile(reTestIdentifier).FindStringSubmatch(*comment.Body); len(testNameFromComment) >= 2 && issueIdentity == testNameFromComment[1] { + targetComment = comments[i] + break + } + } + if nil == targetComment { + return nil, fmt.Errorf("no comment match") + } + return targetComment, nil +} + +// getFlakyIssues filters all issues by flakyLabel, and return map {testName: slice of issues} +// Fail if find any issue with no discoverable identifier(testIdentifierPattern missing), +// also fail if auto comment not found. +// In most cases there is only 1 issue for each testName, if multiple issues found open for same test, +// most likely it's caused by old issues being reopened manually, in this case update both issues. +func getFlakyIssues() (map[string][]*flakyIssue, error) { + issuesMap := make(map[string][]*flakyIssue) + reposForIssue, err := ghutil.ListRepos(org) + if nil != err { + return nil, err + } + for _, repoForIssue := range reposForIssue { + issues, err := ghutil.ListIssuesByRepo(org, repoForIssue, []string{flakyLabel}) + if nil != err { + return nil, err + } + for _, issue := range issues { + if nil != issue.ClosedAt && issue.ClosedAt.Before(timeConsiderOld) { + continue // Issue closed long time ago, it might fail with a different reason now. + } + issueIdentity := regexp.MustCompile(reTestIdentifier).FindStringSubmatch(issue.GetBody()) + if len(issueIdentity) < 2 { // Malformed issue, all auto flaky issues need to be identifiable. + return nil, fmt.Errorf("cannot get test identifier from auto flaky issue '%v'", err) + } + autoComment, err := findExistingComment(issue, issueIdentity[1]) + if nil != err { + return nil, fmt.Errorf("cannot find auto comment for issue '%s': '%v'", *issue.URL, err) + } + issuesMap[issueIdentity[1]] = append(issuesMap[issueIdentity[1]], &flakyIssue{ + issue: issue, + identity: &issueIdentity[1], + comment: autoComment, + }) + } + } + // Handle test with multiple issues associated + // if all open: update all of them + // if all closed: only keep latest one + // otherwise(these may have been closed manually): remove closed ones from map + for k, v := range issuesMap { + hasOpen := false + hasClosed := false + for _, fi := range v { + switch fi.issue.GetState() { + case string(ghutil.IssueOpenState): + hasOpen = true + case string(ghutil.IssueCloseState): + hasClosed = true + } + } + if hasOpen && hasClosed { + for i, fi := range v { + if string(ghutil.IssueCloseState) == fi.issue.GetState() { + issuesMap[k] = append(issuesMap[k][:i], issuesMap[k][i+1:]...) + } + } + } else if !hasOpen { + sort.Slice(issuesMap[k], func(i, j int) bool { + return nil != issuesMap[k][i].issue.CreatedAt && + (nil == issuesMap[k][j].issue.CreatedAt || issuesMap[k][i].issue.CreatedAt.Before(*issuesMap[k][j].issue.CreatedAt)) + }) + issuesMap[k] = []*flakyIssue{issuesMap[k][0]} + } + } + return issuesMap, err +} + +// processGithubIssueForRepo reads RepoData and existing issues, and create/close/reopen/comment on issues. +// The function returns a slice of messages containing performed actions, and a slice of error messages, +// these can later on be printed as summary at the end of run +func processGithubIssueForRepo(rd *RepoData, flakyIssuesMap map[string][]*flakyIssue, dryrun *bool) ([]string, error) { + var messages []string + var errs []error + + // If there are too many failures, create a single issue tracking it. + flakyRate, err := getFlakyRate(rd.TestStats) + if nil != err { + return nil, err + } + if flakyRate > threshold { + log.Printf("flaky rate above '%f', creating a single issue", threshold) + identity := fmt.Sprintf("%.2f%% tests failed in repo %s on %s", + flakyRate*100, rd.Config.Repo, time.Unix(*rd.LastBuildStartTime, 0).String()) + if _, ok := flakyIssuesMap[identity]; ok { + log.Printf("issue already exist, skip creating") + return nil, nil + } + title := fmt.Sprintf("[flaky] %s", identity) + comment := fmt.Sprintf("Bulk issue tracking: %s\n%s", identity, fmt.Sprintf(testIdentifierPattern, identity)) + message := fmt.Sprintf("Creating issue '%s' in repo '%s'", title, repoForIssue) + log.Println(message) + return []string{message}, createNewIssue(org, repoForIssue, title, + fmt.Sprintf(issueBodyTemplate, identity, fmt.Sprintf(testIdentifierPattern, identity)), + comment, dryrun) + } + + // Update/Create issues for flaky/used-to-be-flaky tests + for testFullName, ts := range rd.TestStats { + if !ts.hasEnoughRuns() || (!ts.isFlaky() && !ts.isPassed()) { + continue + } + identity := fmt.Sprintf("'%s' in repo '%s'", testFullName, rd.Config.Repo) + comment := createCommentForTest(rd, testFullName) + if existIssues, ok := flakyIssuesMap[identity]; ok { // update issue with current result + for _, existIssue := range existIssues { + if strings.Contains(existIssue.comment.GetBody(), comment) { + log.Printf("skip updating issue '%s', as it already contains data for run '%d'\n", + *existIssue.issue.URL, *rd.LastBuildStartTime) + continue + } + comment += createHistoryUnicode(rd, existIssue.comment.GetBody(), testFullName) + message := fmt.Sprintf("Updating issue '%s' for '%s'", *existIssue.issue.URL, *existIssue.identity) + log.Println(message) + messages = append(messages, message) + if err := updateIssue(existIssue, comment, ts, dryrun); nil != err { + log.Println(err) + errs = append(errs, err) + } + } + } else if ts.isFlaky() { + title := fmt.Sprintf("[flaky] %s", identity) + comment += createHistoryUnicode(rd, "", testFullName) + comment += "\n" + fmt.Sprintf(testIdentifierPattern, identity) + message := fmt.Sprintf("Creating issue '%s' in repo '%s'", title, repoForIssue) + log.Println(message) + messages = append(messages, message) + if err := createNewIssue(org, repoForIssue, title, + fmt.Sprintf(issueBodyTemplate, identity, fmt.Sprintf(testIdentifierPattern, identity)), + comment, dryrun); nil != err { + log.Println(err) + errs = append(errs, err) + } + } + } + return messages, combineErrors(errs) +} + +// analyze all results, figure out flaky tests and processing existing auto:flaky issues +func processGithubIssues(repoDataAll []*RepoData, githubToken *string, dryrun *bool) error { + messagesMap := make(map[string][]string) + errMap := make(map[string][]error) + var err error + ghutil.Authenticate(githubToken) + currentUser, err = ghutil.GetCurrentUser() + if nil != err { + log.Fatalf("cannot get current authenticated username '%v'", err) + } + + // Collect all flaky test issues from all knative repos, in case issues are moved around + // Fail this job if data collection failed + flakyGHIssuesMap, err := getFlakyIssues() + if nil != err { + log.Fatalf("%v", err) + } + + for _, rd := range repoDataAll { + messages, err := processGithubIssueForRepo(rd, flakyGHIssuesMap, dryrun) + messagesMap[rd.Config.Repo] = messages + if nil != err { + errMap[rd.Config.Repo] = append(errMap[rd.Config.Repo], err) + } + } + + // Print summaries + summary := "Summary:\n" + for _, rd := range repoDataAll { + if messages, ok := messagesMap[rd.Config.Repo]; ok { + summary += fmt.Sprintf("Summary of repo '%s':\n", rd.Config.Repo) + summary += strings.Join(messages, "\n") + } + if errs, ok := errMap[rd.Config.Repo]; ok { + summary += fmt.Sprintf("Errors in repo '%s':\n%v", rd.Config.Repo, combineErrors(errs)) + } + } + log.Println(summary) + return nil +} diff --git a/tools/flaky-test-reporter/main.go b/tools/flaky-test-reporter/main.go index 008fc57dc7..679d77ceb6 100644 --- a/tools/flaky-test-reporter/main.go +++ b/tools/flaky-test-reporter/main.go @@ -30,6 +30,7 @@ import ( func main() { serviceAccount := flag.String("service-account", os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"), "JSON key file for service account to use") + githubToken := flag.String("github-token", "", "Token file for Github authentication") dryrun := flag.Bool("dry-run", false, "dry run switch") flag.Parse() @@ -37,6 +38,7 @@ func main() { log.Printf("running in [dry run mode]") } + var repoDataAll []*RepoData prow.Initialize(*serviceAccount) // Explicit authenticate with gcs Client // Clean up local artifacts directory, this will be used later for artifacts uploads @@ -50,7 +52,6 @@ func main() { log.Fatalf("Failed preparing local artifacts directory: %v", err) } - var repoDataAll []*RepoData for _, jc := range jobConfigs { log.Printf("collecting results for repo '%s'\n", jc.Repo) rd, err := collectTestResultsForRepo(&jc) @@ -62,6 +63,6 @@ func main() { } repoDataAll = append(repoDataAll, rd) } - - // TODO(chaodaiG): pass repoDataAll to functions for Github issues tracking and Slack notification + + processGithubIssues(repoDataAll, githubToken, dryrun) } diff --git a/tools/flaky-test-reporter/result.go b/tools/flaky-test-reporter/result.go index ba5bced3af..5a0837d88b 100644 --- a/tools/flaky-test-reporter/result.go +++ b/tools/flaky-test-reporter/result.go @@ -32,6 +32,13 @@ import ( "github.com/knative/test-infra/shared/junit" ) +const ( + flakyStatus = "Flaky" + passedStatus = "Passed" + lackDataStatus = "NotEnoughData" + failedStatus = "Failed" +) + // RepoData struct contains all configurations and test results for a repo type RepoData struct { Config *JobConfig @@ -68,7 +75,20 @@ func (ts *TestStat) hasEnoughRuns() bool { return len(ts.Passed) + len(ts.Failed) >= requiredCount } -func getFlakyRate(testStats map[string]TestStat) (float32, error) { +func (ts *TestStat) getTestStatus() string { + switch { + case ts.isFlaky(): + return flakyStatus + case ts.isPassed(): + return passedStatus + case !ts.hasEnoughRuns(): + return lackDataStatus + default: + return failedStatus + } +} + +func getFlakyRate(testStats map[string]*TestStat) (float32, error) { totalCount := len(testStats) if 0 == totalCount { return 0.0, nil @@ -167,6 +187,34 @@ func collectTestResultsForRepo(jc *JobConfig) (*RepoData, error) { return rd, nil } +func (rd *RepoData) getResultSliceForTest(testName string) []junit.TestStatusEnum { + res := make([]junit.TestStatusEnum, len(rd.BuildIDs), len(rd.BuildIDs)) + ts := rd.TestStats[testName] + for i, buildID := range rd.BuildIDs { + switch { + case true == intSliceContains(ts.Failed, buildID): + res[i] = junit.Failed + case true == intSliceContains(ts.Passed, buildID): + res[i] = junit.Passed + default: + res[i] = junit.Skipped + } + } + return res +} + +func intSliceContains(its []int, target int) bool { + if nil == its { + return false + } + for _, it := range its { + if it == target { + return true + } + } + return false +} + // getLatestFinishedBuilds is an inexpensive way of listing latest finished builds, in comparing to // the GetLatestBuilds function from prow package, as it doesn't precompute start/finish time before sorting. // This function takes the assumption that build IDs are always incremental integers, it would fail if it doesn't diff --git a/tools/flaky-test-reporter/run.go b/tools/flaky-test-reporter/run.go new file mode 100644 index 0000000000..5dc339a7ae --- /dev/null +++ b/tools/flaky-test-reporter/run.go @@ -0,0 +1,32 @@ +/* +Copyright 2019 The Knative 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. +*/ + +// run.go controls how to run functions that needs dryrun support + +package main + +import ( + "log" +) + +func run(message string, call func() error, dryrun *bool) error { + if nil != dryrun && true == *dryrun { + log.Printf("[dry run] %s", message) + return nil + } + log.Printf(message) + return call() +}