blob: 226ed2225c09f7b003a948bdf9ad006fa461e560 [file] [log] [blame]
package metrics
import (
"bytes"
"encoding/json"
"fmt"
"log"
"regexp"
"strconv"
"strings"
"github.com/bazelbuild/continuous-integration/metrics/clients"
"github.com/bazelbuild/continuous-integration/metrics/data"
)
type Flakiness struct {
client *clients.CloudStorageClient
columns []Column
gcsBucket string
gcsSuffix string
pipelines []*data.PipelineID
}
func (f *Flakiness) Name() string {
return "flakiness"
}
func (f *Flakiness) Columns() []Column {
return f.columns
}
func (f *Flakiness) Collect() (data.DataSet, error) {
result := data.CreateDataSet(GetColumnNames(f.columns))
for _, pipeline := range f.pipelines {
gcsPath := f.gcsSuffix + pipeline.Slug
iter, err := f.client.ReadAllFiles(f.gcsBucket, gcsPath)
if err != nil {
return nil, err
}
if !iter.HasNext() {
log.Printf("There is no flakiness data for pipeline %s in GCS location %s/%s", pipeline, f.gcsBucket, gcsPath)
continue
}
for iter.HasNext() {
fileName, content, err := iter.Get()
if err != nil {
return nil, fmt.Errorf("Failed to read flakiness data for pipeline %s: %v", pipeline, err)
}
build, err := getBuildNumber(fileName)
if err != nil {
return nil, fmt.Errorf("Invalid flaky data file %s in %s/%s: %v", fileName, f.gcsBucket, gcsPath, err)
}
messages, err := getFlakyMessages(content)
if err != nil {
return nil, fmt.Errorf("Failed to parse flaky data file %s in %s/%s: %v", fileName, f.gcsBucket, gcsPath, err)
}
for _, msg := range messages {
err = result.AddRow(pipeline.Org, pipeline.Slug, build, msg.ID.Summary.Label, len(msg.Summary.Passed), len(msg.Summary.Failed))
if err != nil {
return nil, fmt.Errorf("Failed to add flakiness data for build #%d of pipeline %s: %v", build, pipeline, err)
}
}
}
}
return result, nil
}
func getFlakyMessages(content []byte) ([]message, error) {
messages := make([]message, 0)
for _, line := range bytes.Split(content, []byte("\n")) {
if len(line) == 0 {
continue
}
var msg message
err := json.Unmarshal([]byte(line), &msg)
if err != nil {
return nil, fmt.Errorf("JSON parse error: %v. Line:\n%s", err, line)
}
if msg.Summary.Status == "FLAKY" {
messages = append(messages, msg)
}
}
return messages, nil
}
func getBuildNumber(fileName string) (int, error) {
re := regexp.MustCompile(`(\d+).json$`)
matches := re.FindStringSubmatch(fileName)
if len(matches) == 2 {
if build, err := strconv.Atoi(matches[1]); err == nil {
return build, nil
}
}
return 0, fmt.Errorf("Flakiness data file names must be 'some_path/<build_number.json>'. Invalid given value: '%s'", fileName)
}
type message struct {
ID struct {
Summary struct {
Label string `json:"label"`
} `json:"testSummary"`
}
Summary struct {
Passed []struct{ uri string } `json:"passed"`
Failed []struct{ uri string } `json:"failed"`
Status string `json:"overallStatus"`
} `json:"testSummary"`
}
// CREATE TABLE flakiness (org VARCHAR(255), pipeline VARCHAR(255), build INT, target VARCHAR(255), passed_count INT, failed_count INT, PRIMARY KEY(org, pipeline, build, target));
func CreateFlakiness(client *clients.CloudStorageClient, gcsBucket, gcsBasePath string, pipelines ...*data.PipelineID) *Flakiness {
columns := []Column{Column{"org", true}, Column{"pipeline", true}, Column{"build", true}, Column{"target", true}, Column{"passed_count", false}, Column{"failed_count", false}}
gcsSuffix := gcsBasePath
if !strings.HasSuffix(gcsBasePath, "/") {
gcsSuffix = gcsBasePath + "/"
}
return &Flakiness{client: client, columns: columns, gcsBucket: gcsBucket, gcsSuffix: gcsSuffix, pipelines: pipelines}
}