blob: d8fc1d7039042ad4d09a6d2470fe53d316c75d43 [file] [log] [blame]
package tryjobstore
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
"cloud.google.com/go/datastore"
"go.skia.org/infra/go/buildbucket"
"go.skia.org/infra/go/paramtools"
)
// TODO(stephana): Move the UNKNOWN status to the first spot, so that we can
// move to a "higher" status easily.
// States of a tryjob in increasing order.
const (
TRYJOB_SCHEDULED TryjobStatus = iota
TRYJOB_RUNNING
TRYJOB_COMPLETE
TRYJOB_INGESTED
TRYJOB_FAILED
TRYJOB_UNKNOWN
)
// statusStringRepr maps from a TryjobStatus to a string.
var statusStringRepr = []string{
"scheduled",
"running",
"complete",
"ingested",
"failed",
"unknown",
}
// statusStringMap maps from status strings to instances of TryjobStatus
var statusStringMap = map[string]TryjobStatus{}
func init() {
// Initialize the mapping between TryjobStatus and it's string representation.
for idx, repr := range statusStringRepr {
statusStringMap[repr] = TryjobStatus(idx)
}
}
// TryjobStatus is an enum that captures the status of a tryjob.
type TryjobStatus int
// String returns a tryjob status as a string.
func (t TryjobStatus) String() string {
return statusStringRepr[t]
}
// tryjobStatusFromString translates from a string representation of TryjobStatus
// to the integer representation.
func tryjobStatusFromString(statusStr string) TryjobStatus {
if s, ok := statusStringMap[statusStr]; ok {
return s
}
return TRYJOB_UNKNOWN
}
// Serialize TryjobStatus as string to JSON.
func (t TryjobStatus) MarshalJSON() ([]byte, error) {
return []byte("\"" + t.String() + "\""), nil
}
// Deserialize a TryjobStatus from JSON.
func (t *TryjobStatus) UnmarshalJSON(data []byte) error {
strStatus := strings.Trim(string(data), "\"")
*t = tryjobStatusFromString(strStatus)
return nil
}
// Reuse types from the buildbucket package.
type Parameters = buildbucket.Parameters
type Properties = buildbucket.Properties
// newerInterface is an internal interface that allows to define a temporal
// order for a type.
type newerInterface interface {
newer(right interface{}) bool
}
// TODO(stephana): Fix the Committed field below to also be spelled correctly
// in the database.
// Issue captures information about a single code review issue.
type Issue struct {
ID int64 `json:"id"`
Subject string `json:"subject" datastore:",noindex"`
Owner string `json:"owner"`
Updated time.Time `json:"updated"`
URL string `json:"url" datastore:",noindex"`
Status string `json:"status"`
PatchsetDetails []*PatchsetDetail `json:"patchsets" datastore:",noindex"`
Committed bool `json:"committed" datastore:"Commited"`
QueryPatchsets []int64 `json:"queryPatchsets" datastore:"-"`
CommentAdded bool `json:"-" datastore:",noindex"`
clean bool
}
// MarshalJSON implements the Marshaller interface in encoding/json.
func (is *Issue) MarshalJSON() ([]byte, error) {
// Create a wrapping struct around the Tryjob that produces the correct output.
temp := struct {
*wrapIssue
Updated TimeJsonMs `json:"updated"` // Override the Updated field to produce a timestamp in MS.
}{
wrapIssue: (*wrapIssue)(is),
Updated: TimeJsonMs(is.Updated),
}
return json.Marshal(&temp)
}
// wrapIssue is a dummy type to avoid recursive call to Tryjob.MarshallJSON
type wrapIssue Issue
// HasPatchset returns true if the issue has the given patchset.
func (is *Issue) HasPatchset(patchsetID int64) bool {
if is == nil {
return false
}
found := is.FindPatchset(patchsetID)
return found != nil
}
// FindPatchset returns the with the given id or nil if cannot be found
func (is *Issue) FindPatchset(id int64) *PatchsetDetail {
for _, psd := range is.PatchsetDetails {
if psd.ID == id {
return psd
}
}
return nil
}
// UpdatePatchset merges the given patchset information into this issue.
func (is *Issue) UpdatePatchsets(patchsets []*PatchsetDetail) {
if is.PatchsetDetails == nil {
is.PatchsetDetails = make([]*PatchsetDetail, 0, len(patchsets))
}
// fmt.Printf("patchsets: %s", spew.Sdump(patchsets))
for _, psd := range patchsets {
// Only insert the patchset if it's not already there.
if found := is.FindPatchset(psd.ID); found == nil {
is.clean = false
// insert patchset in the right spot.
is.PatchsetDetails = append(is.PatchsetDetails, psd)
}
}
if !is.clean {
sort.Slice(is.PatchsetDetails, func(i, j int) bool { return is.PatchsetDetails[i].ID < is.PatchsetDetails[j].ID })
}
}
// newer implements newerInterface.
func (is *Issue) newer(right interface{}) bool {
return is.Updated.After(right.(*Issue).Updated)
}
// PatchsetDetails accumulates information about one patchset and the connected
// tryjobs.
type PatchsetDetail struct {
ID int64 `json:"id"`
Commit string `json:"commit"`
ParentCommit string `json:"parentCommit"`
Tryjobs []*Tryjob `json:"tryjobs" datastore:"-"`
}
// Tryjob captures information about a tryjob in BuildBucket.
type Tryjob struct {
Key *datastore.Key `json:"-" datastore:"__key__"` // Insert the key upon loading
BuildBucketID int64 `json:"buildBucketID"`
IssueID int64 `json:"issueID"`
PatchsetID int64 `json:"patchsetID"`
Builder string `json:"builder"`
Status TryjobStatus `json:"status"`
Updated time.Time `json:"-"`
MasterCommit string `json:"masterCommit"`
}
// clone returns a shallow copy of the Tryjob instance
func (t *Tryjob) clone() *Tryjob {
ret := &Tryjob{}
*ret = *t
return ret
}
type TimeJsonMs time.Time
func (j TimeJsonMs) MarshalJSON() ([]byte, error) {
val := time.Time(j).UnixNano() / int64(time.Millisecond)
return json.Marshal(val)
}
// String returns a string representation for the Tryjob
func (t *Tryjob) String() string {
return fmt.Sprintf("%s - %d - %s", t.Builder, t.BuildBucketID, t.Status.String())
}
// newer implements newerInterface.
func (t *Tryjob) newer(r interface{}) bool {
right := r.(*Tryjob)
// A tryjob is newer if the status is updated or the BuildBucket record has been
// updated.
return t.Updated.Before(right.Updated) || (t.Status > right.Status)
}
// TryjobResult stores results. It is stored in the database as a child of
// a Tryjob entity.
type TryjobResult struct {
TestName string `datastore:"TestName,noindex"`
Digest string `datastore:"Digest,noindex"`
Params paramtools.ParamSet `datastore:"-"`
}
const (
tjrParamPrefix = "param."
)
// Save implements the datastore.PropertyLoadSaver interface.
func (t *TryjobResult) Save() ([]datastore.Property, error) {
props, err := datastore.SaveStruct(t)
if err != nil {
return nil, err
}
// Make it large enough to hold the struct props and the parameters.
ret := make([]datastore.Property, len(t.Params), len(props)+len(t.Params))
idx := 0
for param, value := range t.Params {
ret[idx].Name = tjrParamPrefix + param
ret[idx].Value = strToInterfaceSlice(value)
ret[idx].NoIndex = true
idx += 1
}
ret = append(ret, props...)
return ret, nil
}
// Load implements the datastore.PropertyLoadSaver interface.
func (t *TryjobResult) Load(props []datastore.Property) error {
nonParams := make([]datastore.Property, 0, 2)
t.Params = make(paramtools.ParamSet, len(props)-2)
for _, prop := range props {
if strings.HasPrefix(prop.Name, tjrParamPrefix) {
t.Params[strings.TrimPrefix(prop.Name, tjrParamPrefix)] = interfaceToStrSlice(prop.Value.([]interface{}))
} else {
nonParams = append(nonParams, prop)
}
}
return datastore.LoadStruct(t, nonParams)
}
// strToInterfaceSlice copies a slice of string to a slice of interface{}.
func strToInterfaceSlice(inArr []string) []interface{} {
ret := make([]interface{}, len(inArr), len(inArr))
for idx, val := range inArr {
ret[idx] = val
}
return ret
}
// interfaceToStrSlice copies a slice of interface{} to a string slice.
func interfaceToStrSlice(inArr []interface{}) []string {
ret := make([]string, len(inArr), len(inArr))
for idx, val := range inArr {
ret[idx] = val.(string)
}
return ret
}
// IssueExpChange is used as the event type when tryjob related information changes
// and an event is sent to notify client.
type IssueExpChange struct {
IssueID int64 `json:"issueID"`
}