mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
156 lines
5.2 KiB
Go
156 lines
5.2 KiB
Go
// 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]
|
|
}
|