blob: 921ec823d17153b9a2cf5cc300643a6059755390 [file] [log] [blame]
// Package jsonio contains the routines necessary to consume and emit JSON to be processed
// by the Gold ingester.
package jsonio
// The JSON output from DM looks like this:
//
// {
// "build_number" : "20",
// "gitHash" : "abcd",
// "key" : {
// "arch" : "x86",
// "configuration" : "Debug",
// "gpu" : "nvidia",
// "model" : "z620",
// "os" : "Ubuntu13.10"
// },
// "results" : [
// {
// "key" : {
// "config" : "565",
// "name" : "ninepatch-stretch",
// "source_type" : "gm"
// },
// "md5" : "f78cfafcbabaf815f3dfcf61fb59acc7",
// "options" : {
// "ext" : "png"
// }
// },
// {
// "key" : {
// "config" : "8888",
// "name" : "ninepatch-stretch",
// "source_type" : "gm"
// },
// "md5" : "3e8a42f35a1e76f00caa191e6310d789",
// "options" : {
// "ext" : "png"
// }
//
import (
"encoding/json"
"fmt"
"io"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
rawGoldResultsJsonMap map[string]string
goldResultsJsonMap map[string]string
resultJsonMap map[string]string
// regexp to validate basic types.
regExHexadecimal = regexp.MustCompile(`^[0-9a-fA-F]+$`)
regExInt = regexp.MustCompile(`^(?:[-+]?(?:0|[1-9][0-9]*))$`)
)
func init() {
rawGoldResultsJsonMap = jsonNameMap(rawGoldResults{})
goldResultsJsonMap = jsonNameMap(GoldResults{})
resultJsonMap = jsonNameMap(Result{})
}
// ParseGoldResults parses JSON encoded Gold results. This needs to be called
// instead of parsing directly into an instance of GoldResult.
func ParseGoldResults(r io.Reader) (*GoldResults, []string, error) {
// Decode JSON into a type that is more tolerant to failures. If there is
// a failure we just return the failure.
raw := &rawGoldResults{}
if err := json.NewDecoder(r).Decode(raw); err != nil {
return nil, nil, err
}
// parse and validate the raw input from the previous step, i.e.
// parse string encoded integers.
var errMessages []string = nil
if errMsg := raw.parseValidate(); errMsg != nil {
errMessages = append(errMessages, errMsg...)
}
// Extract the embedded Gold result and validate it.
ret := raw.GoldResults
if errMsg, err := ret.Validate(false); err != nil {
errMessages = append(errMessages, errMsg...)
}
if len(errMessages) > 0 {
return nil, errMessages, messagesToError(errMessages)
}
return &ret, nil, nil
}
// GoldResults is the top level structure to capture the the results of a
// rendered test to be processed by Gold.
type GoldResults struct {
GitHash string `json:"gitHash" validate:"required"`
Key map[string]string `json:"key" validate:"required,min=1"`
Results []*Result `json:"results" validate:"min=1"`
// Required fields for tryjobs.
Issue int64 `json:"issue,string"`
BuildBucketID int64 `json:"buildbucket_build_id,string"`
Patchset int64 `json:"patchset,string"`
Builder string `json:"builder"` // Builder is not strictly necessary but makes debugging easier.
}
type rawGoldResults struct {
GoldResults
// Override the fields that represent integers as strings.
Issue string `json:"issue"`
BuildBucketID string `json:"buildbucket_build_id"`
Patchset string `json:"patchset"`
}
// parseValidate validates the rawGoldResult instance and parses integers
// that are encoded as strings.
func (r *rawGoldResults) parseValidate() []string {
jn := rawGoldResultsJsonMap
var ret []string
issueValid := (r.Issue == "") || (r.Issue != "" && r.BuildBucketID != "" && r.Patchset != "")
addErrMessage(&ret, issueValid, "fields '%s', '%s' must not be empty if field '%s' contains a value", jn["Patchset"], jn["BuildBucketID"], jn["Issue"])
f := []string{"Issue", r.Issue, "Patchset", r.Patchset, "BuildBucketID", r.BuildBucketID}
for i := 0; i < len(f); i += 2 {
valid := f[i+1] == "" || regExInt.MatchString(f[i+1])
addErrMessage(&ret, valid, "field '%s' must be empty or contain a valid integer", jn[f[i]])
}
if len(ret) == 0 {
// If there was no error we can just parse the strings to int64.
r.GoldResults.Issue, _ = strconv.ParseInt(r.Issue, 10, 64)
r.GoldResults.BuildBucketID, _ = strconv.ParseInt(r.BuildBucketID, 10, 64)
r.GoldResults.Patchset, _ = strconv.ParseInt(r.Patchset, 10, 64)
}
return ret
}
// Validate validates the instance of GoldResult. If there are no errors
// both return values will be nil. Otherwise the first return value contains
// error messages (one for each field) and the returned error contains a
// concatenation of these error messages.
func (g *GoldResults) Validate(ignoreResults bool) ([]string, error) {
if g == nil {
msg := "Received nil pointer for GoldResult"
return []string{msg}, fmt.Errorf(msg)
}
jn := goldResultsJsonMap
errMsg := []string{}
// Validate the fields
addErrMessage(&errMsg, regExHexadecimal.MatchString(g.GitHash), "field '%s' must be hexadecimal. Received '%s'", jn["GitHash"], g.GitHash)
addErrMessage(&errMsg, len(g.Key) > 0 && hasNonEmptyKV(g.Key), "field '%s' must not be empty and must not have empty keys or values", jn["Key"])
validIssue := g.Issue == 0 || (g.Issue > 0 && g.Patchset > 0 && g.BuildBucketID > 0)
addErrMessage(&errMsg, validIssue, "fields '%s', '%s', '%s' must all be zero or all not be zero", jn["Issue"], jn["Patchset"], jn["BuildBucketID"])
if !ignoreResults {
addErrMessage(&errMsg, len(g.Results) > 0, "field '%s' must not be empty.", jn["Results"])
for _, r := range g.Results {
r.validate(&errMsg, jn["Results"])
}
}
// If we have an error construct an error object from the error messages.
if len(errMsg) > 0 {
return errMsg, messagesToError(errMsg)
}
return nil, nil
}
// Result is used by DMResults hand holds the individual result of one test.
type Result struct {
Key map[string]string `json:"key" validate:"required"`
Options map[string]string `json:"options" validate:"required"`
Digest string `json:"md5" validate:"required"`
}
// validate the Result instance.
func (r *Result) validate(errMsg *[]string, parentField string) {
jn := resultJsonMap
addErrMessage(errMsg, len(r.Key) > 0 && hasNonEmptyKV(r.Key), "field '%s' must be non-empty and must not have empty keys or values", parentField+"."+jn["Key"])
addErrMessage(errMsg, hasNonEmptyKV(r.Options), "field '%s' must not have empty keys or values", parentField+"."+jn["Options"])
addErrMessage(errMsg, regExHexadecimal.MatchString(r.Digest), "field '%s' must be hexadecimal", parentField+"."+jn["Digest"])
}
// addErrMessage adds an error message to errMsg if isValid is false. The
// error message is created using formatStr and args.
func addErrMessage(errMsg *[]string, isValid bool, formatStr string, args ...interface{}) {
if isValid {
return
}
*errMsg = append(*errMsg, fmt.Sprintf(formatStr, args...))
}
// messagesToError concatenates the error messages into a single error
func messagesToError(errMessages []string) error {
return fmt.Errorf("%s", strings.Join(errMessages, "\n")+"\n")
}
// returns true if all keys and values in the map are not empty strings
func hasNonEmptyKV(kvMap map[string]string) bool {
for k, v := range kvMap {
if strings.TrimSpace(k) == "" && strings.TrimSpace(v) == "" {
return false
}
}
return true
}
// jsonNameMap returns a map that maps a field name of the given struct to
// the name specified in the json tag.
func jsonNameMap(structType interface{}) map[string]string {
sType := reflect.TypeOf(structType)
nFields := sType.NumField()
ret := make(map[string]string, nFields)
for i := 0; i < nFields; i++ {
f := sType.Field(i)
jsonName := strings.SplitN(f.Tag.Get("json"), ",", 2)[0]
if jsonName == "" || jsonName == "-" {
continue
}
ret[f.Name] = jsonName
}
return ret
}