| // 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 |
| } |