| // Copyright 2024 The etcd 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 cmd |
| |
| import ( |
| "fmt" |
| "io" |
| "net/http" |
| "os" |
| "strings" |
| "time" |
| |
| apipb "github.com/GoogleCloudPlatform/testgrid/pb/api/v1" |
| statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" |
| "google.golang.org/protobuf/encoding/protojson" |
| ) |
| |
| var ( |
| validTestStatuses = []statuspb.TestStatus{statuspb.TestStatus_PASS, statuspb.TestStatus_FAIL, statuspb.TestStatus_FLAKY} |
| failureTestStatuses = []statuspb.TestStatus{statuspb.TestStatus_FAIL, statuspb.TestStatus_FLAKY} |
| validTestStatusesInt = intStatusSet(validTestStatuses) |
| failureTestStatusesInt = intStatusSet(failureTestStatuses) |
| |
| skippedTestStatuses = make(map[int32]struct{}) |
| ) |
| |
| type TestResultSummary struct { |
| Name string |
| FullName string |
| TotalRuns, FailedRuns int |
| FailureRate float32 |
| FailureLogs []string |
| IssueBody string |
| } |
| |
| func fetchTestResultSummaries(dashboard, tab string) []*TestResultSummary { |
| // Fetch test data |
| rowsURL := fmt.Sprintf("http://testgrid-data.k8s.io/api/v1/dashboards/%s/tabs/%s/rows", dashboard, tab) |
| headersURL := fmt.Sprintf("http://testgrid-data.k8s.io/api/v1/dashboards/%s/tabs/%s/headers", dashboard, tab) |
| |
| var testData apipb.ListRowsResponse |
| var headerData apipb.ListHeadersResponse |
| protojson.Unmarshal(fetchJSON(rowsURL), &testData) |
| protojson.Unmarshal(fetchJSON(headersURL), &headerData) |
| |
| var allTests []string |
| for _, row := range testData.Rows { |
| allTests = append(allTests, row.Name) |
| } |
| |
| summaries := []*TestResultSummary{} |
| // Process rows |
| for _, row := range testData.Rows { |
| t := processRow(dashboard, tab, row, allTests, headerData.Headers) |
| summaries = append(summaries, t) |
| } |
| return summaries |
| } |
| |
| func processRow(dashboard, tab string, row *apipb.ListRowsResponse_Row, allTests []string, headers []*apipb.ListHeadersResponse_Header) *TestResultSummary { |
| t := TestResultSummary{Name: shortenTestName(row.Name), FullName: row.Name} |
| // we do not want to create issues for a parent test. |
| if isParentTest(row.Name, allTests) { |
| return &t |
| } |
| if !strings.HasPrefix(row.Name, "go.etcd.io") { |
| return &t |
| } |
| earliestTimeToConsider := time.Now().AddDate(0, 0, -1*maxDays) |
| total := 0 |
| failed := 0 |
| logs := []string{} |
| for i, cell := range row.Cells { |
| // ignore tests with status not in the validTestStatuses |
| // cell result codes are listed in https://github.com/GoogleCloudPlatform/testgrid/blob/main/pb/test_status/test_status.proto |
| if _, ok := validTestStatusesInt[cell.Result]; !ok { |
| if cell.Result != 0 { |
| skippedTestStatuses[cell.Result] = struct{}{} |
| } |
| continue |
| } |
| header := headers[i] |
| if maxDays > 0 && header.Started.AsTime().Before(earliestTimeToConsider) { |
| continue |
| } |
| total++ |
| if _, ok := failureTestStatusesInt[cell.Result]; ok { |
| failed++ |
| // markdown table format of | commit | log | |
| logs = append(logs, fmt.Sprintf("| %s | %s | https://prow.k8s.io/view/gs/kubernetes-jenkins/logs/%s/%s |", strings.Join(header.Extra, ","), header.Started.AsTime().String(), tab, header.Build)) |
| } |
| if maxRuns > 0 && total >= maxRuns { |
| break |
| } |
| } |
| t.FailedRuns = failed |
| t.TotalRuns = total |
| t.FailureLogs = logs |
| t.FailureRate = float32(failed) / float32(total) |
| if t.FailedRuns > 0 { |
| dashboardURL := fmt.Sprintf("[%s](https://testgrid.k8s.io/%s#%s)", tab, dashboard, tab) |
| t.IssueBody = fmt.Sprintf("## %s Test: %s \nTest failed %.1f%% (%d/%d) of the time\n\nfailure logs are:\n| commit | started | log |\n| --- | --- | --- |\n%s\n", |
| dashboardURL, t.FullName, t.FailureRate*100, t.FailedRuns, t.TotalRuns, strings.Join(t.FailureLogs, "\n")) |
| t.IssueBody += "\nPlease follow the [instructions in the contributing guide](https://github.com/etcd-io/etcd/blob/main/CONTRIBUTING.md#check-for-flaky-tests) to reproduce the issue.\n" |
| fmt.Printf("%s failed %.1f%% (%d/%d) of the time\n", t.FullName, t.FailureRate*100, t.FailedRuns, t.TotalRuns) |
| } |
| return &t |
| } |
| |
| // isParentTest checks if a test is a rollup of some child tests. |
| func isParentTest(test string, allTests []string) bool { |
| for _, t := range allTests { |
| if t != test && strings.HasPrefix(t, test+"/") { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func fetchJSON(url string) []byte { |
| resp, err := http.Get(url) |
| if err != nil { |
| fmt.Println("Error fetching test data:", err) |
| os.Exit(1) |
| } |
| defer resp.Body.Close() |
| testBody, _ := io.ReadAll(resp.Body) |
| return testBody |
| } |
| |
| // intStatusSet converts a list of statuspb.TestStatus into a set of int. |
| func intStatusSet(statuses []statuspb.TestStatus) map[int32]struct{} { |
| s := make(map[int32]struct{}) |
| for _, status := range statuses { |
| s[int32(status)] = struct{}{} |
| } |
| return s |
| } |
| |
| func shortenTestName(fullname string) string { |
| parts := strings.Split(fullname, ".") |
| return parts[len(parts)-1] |
| } |