blob: 7ec1ff60f74fbc19273493b91eeaa8308d17c522 [file] [log] [blame]
package buildbot
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"time"
"go.skia.org/infra/go/util"
)
/*
Tools for loading data from Buildbot's JSON interface.
*/
const (
BUILDBOT_URL = "http://build.chromium.org/p/"
LOAD_ATTEMPTS = 3
TRYBOT_PATTERN = ".*-Trybot$"
)
var (
TRYBOT_REGEXP = regexp.MustCompile(TRYBOT_PATTERN)
)
// BuildStep contains information about a build step.
type BuildStep struct {
Name string
Number int
Results int
Started time.Time
Finished time.Time
}
// IsStarted returns true iff the BuildStep has started.
func (bs *BuildStep) IsStarted() bool {
return !util.TimeIsZero(bs.Started)
}
// IsFinished returns true iff the BuildStep has finished.
func (bs *BuildStep) IsFinished() bool {
return !util.TimeIsZero(bs.Finished)
}
func (bs BuildStep) Copy() *BuildStep {
return &bs
}
// Build.Results code descriptions, see http://docs.buildbot.net/current/developer/results.html.
const (
BUILDBOT_SUCCESS = 0
BUILDBOT_WARNINGS = 1
BUILDBOT_FAILURE = 2
BUILDBOT_SKIPPED = 3
BUILDBOT_EXCEPTION = 4
// The doc above says that both EXCEPTION and RETRY are value 4.
BUILDBOT_RETRY = 4
)
// Parse s as one of the above values for Build.Results.
func ParseResultsString(s string) (int, error) {
switch strings.ToLower(s) {
case "success":
return BUILDBOT_SUCCESS, nil
case "warnings", "warning":
return BUILDBOT_WARNINGS, nil
case "failure":
return BUILDBOT_FAILURE, nil
case "skipped":
return BUILDBOT_SKIPPED, nil
case "exception":
return BUILDBOT_EXCEPTION, nil
case "retry":
return BUILDBOT_RETRY, nil
default:
return 0, fmt.Errorf("Invalid buildbot Results code: %s", s)
}
}
// Build contains information about a single build.
type Build struct {
Builder string
Master string
Number int
BuildSlave string
Branch string
Commits []string
GotRevision string
Properties [][]interface{}
PropertiesStr string
Results int
Steps []*BuildStep
Started time.Time
Finished time.Time
Comments []*BuildComment
Repository string
}
// Id constructs an ID for the given Build.
func (b *Build) Id() BuildID {
return MakeBuildID(b.Master, b.Builder, b.Number)
}
// jsonBuildStep is a struct used for (de)serializing a BuildStep to JSON.
type jsonBuildStep struct {
Name string `json:"name"`
Times []float64 `json:"times"`
Number int `json:"step_number"`
Results []interface{} `json:"results"`
}
// jsonBuild is a struct used for (de)serializing a Build to JSON.
type jsonBuild struct {
Builder string `json:"builderName"`
Number int `json:"number"`
Properties [][]interface{} `json:"properties"`
Results int `json:"results"`
Steps []*jsonBuildStep `json:"steps"`
Times []float64 `json:"times"`
}
// MarshalJSON serializes the Build to JSON.
func (b *Build) MarshalJSON() ([]byte, error) {
build := jsonBuild{
Builder: b.Builder,
Number: b.Number,
Results: b.Results,
Times: []float64{
util.TimeToUnixFloat(b.Started),
util.TimeToUnixFloat(b.Finished),
},
Properties: b.Properties,
}
steps := make([]*jsonBuildStep, 0, len(b.Steps))
for _, s := range b.Steps {
steps = append(steps, &jsonBuildStep{
Name: s.Name,
Number: s.Number,
Results: []interface{}{
s.Results,
[]interface{}{},
},
Times: []float64{
util.TimeToUnixFloat(s.Started),
util.TimeToUnixFloat(s.Finished),
},
})
}
build.Steps = steps
return json.Marshal(&build)
}
// UnmarshalJSON deserializes the Build from JSON.
func (b *Build) UnmarshalJSON(data []byte) error {
var build jsonBuild
if err := json.NewDecoder(bytes.NewBuffer(data)).Decode(&build); err != nil {
return err
}
b.Builder = build.Builder
b.Number = build.Number
b.Properties = build.Properties
b.Results = build.Results
if len(build.Times) != 2 {
return fmt.Errorf("times array must have length 2: %v", build.Times)
}
b.Started = util.UnixFloatToTime(build.Times[0])
b.Finished = util.UnixFloatToTime(build.Times[1])
// Parse the following from build properties.
var err error
b.Repository, err = b.GetStringProperty("repository")
if err != nil {
return err
}
b.GotRevision, err = b.GetStringProperty("got_revision")
if err != nil {
b.GotRevision = ""
}
b.Branch, err = b.GetStringProperty("branch")
if err != nil {
return err
}
b.BuildSlave, err = b.GetStringProperty("slavename")
if err != nil {
return err
}
b.Master, err = b.GetStringProperty("mastername")
if err != nil {
return err
}
b.Steps = make([]*BuildStep, 0, len(build.Steps))
for _, s := range build.Steps {
if len(s.Times) != 2 {
return fmt.Errorf("times array must have length 2 (step): %v", s.Times)
}
results := 0
if len(s.Results) > 0 {
if s.Results[0] != nil {
results = int(s.Results[0].(float64))
}
}
b.Steps = append(b.Steps, &BuildStep{
Name: s.Name,
Number: s.Number,
Results: results,
Started: util.UnixFloatToTime(s.Times[0]).UTC(),
Finished: util.UnixFloatToTime(s.Times[1]).UTC(),
})
}
b.fixup()
return nil
}
// fixup fixes a Build object before/after deserialization.
func (b *Build) fixup() {
// gob considers empty slices and nil slices to be the same. Create
// empty slices for any that might be nil.
if reflect.ValueOf(b.Comments).IsNil() {
b.Comments = []*BuildComment{}
}
if reflect.ValueOf(b.Commits).IsNil() {
b.Commits = []string{}
}
if reflect.ValueOf(b.Steps).IsNil() {
b.Steps = []*BuildStep{}
}
// Ensure that all times are in UTC.
b.Started = b.Started.UTC()
b.Finished = b.Finished.UTC()
for _, s := range b.Steps {
s.Started = s.Started.UTC()
s.Finished = s.Finished.UTC()
}
for _, c := range b.Comments {
c.Timestamp = c.Timestamp.UTC()
}
// Sort the commits alphabetically, for convenience.
sort.Strings(b.Commits)
}
func (b Build) Copy() *Build {
b.Commits = append([]string{}, b.Commits...)
props := [][]interface{}{}
for _, p := range b.Properties {
props = append(props, append([]interface{}{}, p...))
}
b.Properties = props
steps := []*BuildStep{}
for _, s := range b.Steps {
steps = append(steps, s.Copy())
}
b.Steps = steps
comments := []*BuildComment{}
for _, c := range b.Comments {
comments = append(comments, c.Copy())
}
b.Comments = comments
return &b
}
// Builder contains information about a builder.
type Builder struct {
Name string
Master string
PendingBuilds int
Slaves []string
State string
}
// BuildSlave contains information about a buildslave.
type BuildSlave struct {
Builders map[string][]int
Connected bool
Name string
Master string
RunningBuilds []interface{}
}
// BuildComment contains a comment about a build.
type BuildComment struct {
Id int64 `json:"id"`
User string `json:"user"`
Timestamp time.Time `json:"time"`
Message string `json:"message"`
}
func (bc BuildComment) Copy() *BuildComment {
return &bc
}
// BuilderComment contains a comment about a builder.
type BuilderComment struct {
Id int64 `json:"id"`
Builder string `json:"builder"`
User string `json:"user"`
Timestamp time.Time `json:"time"`
Flaky bool `json:"flaky"`
IgnoreFailure bool `json:"ignoreFailure"`
Message string `json:"message"`
}
// CommitComment contains a comment about a commit.
type CommitComment struct {
Id int64 `json:"id"`
Commit string `json:"commit"`
User string `json:"user"`
Timestamp time.Time `json:"time"`
IgnoreFailure bool `json:"ignoreFailure"`
Message string `json:"message"`
}
// IsTrybot determines whether the given builder is a trybot.
func IsTrybot(b string) bool {
return TRYBOT_REGEXP.MatchString(b)
}
// GetProperty returns the value for a build property, if it exists. Otherwise returns an error.
func (b *Build) GetProperty(property string) (interface{}, error) {
for _, propVal := range b.Properties {
if len(propVal) >= 2 {
key, ok := propVal[0].(string)
if ok && key == property {
return propVal[1], nil
}
}
}
return nil, fmt.Errorf("No such property %s", property)
}
// GetStringProperty returns the value for a build property if it exists and it is a string. Otherwise returns an error.
func (b *Build) GetStringProperty(property string) (string, error) {
val, err := b.GetProperty(property)
if err != nil {
return "", err
}
// It's okay for the property to be unset.
if val == nil {
return "", nil
}
strVal, ok := val.(string)
if !ok {
return "", fmt.Errorf("Not a string property %s", property)
}
return strVal, nil
}
// getPropertyInterface returns an interface value for the given property.
func getPropertyInterface(propname string, value interface{}) []interface{} {
return []interface{}{
propname,
value,
"fake_source",
}
}
// IsStarted indicates whether the build has started.
func (b *Build) IsStarted() bool {
return !util.TimeIsZero(b.Started)
}
// IsFinished indicates whether the build has finished.
func (b *Build) IsFinished() bool {
return !util.TimeIsZero(b.Finished)
}
// GetSummary returns a BuildSummary for the given Build.
func (b *Build) GetSummary() *BuildSummary {
steps := make([]string, 0, len(b.Steps))
for _, s := range b.Steps {
if s.Results != 0 && s.Name != "steps" {
steps = append(steps, s.Name)
}
}
return &BuildSummary{
Builder: b.Builder,
BuildSlave: b.BuildSlave,
FailedSteps: steps,
Finished: b.IsFinished(),
GotRevision: b.GotRevision,
Master: b.Master,
Number: b.Number,
Properties: b.Properties,
Results: b.Results,
Comments: b.Comments,
Commits: b.Commits,
}
}
// BuildSummary is a struct which contains the minimal amount of information
// that we care to see on the dashboard.
type BuildSummary struct {
Builder string `json:"builder"`
BuildSlave string `json:"buildslave"`
FailedSteps []string `json:"failedSteps"`
Finished bool `json:"finished"`
GotRevision string `json:"got_revision"`
Master string `json:"master"`
Number int `json:"number"`
Properties [][]interface{} `json:"properties"`
Results int `json:"results"`
Comments []*BuildComment `json:"comments"`
Commits []string `json:"commits"`
}