blob: 4af6806392fdd0491515fd5b8131a7d54220bc81 [file]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package model
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
)
// ResultsVersion is the latest version of the JSON format that "results.json"
// and "results-small.json" files are using.
const ResultsVersion = 4
var (
// CleanPrefix is the prefix that CleanJSON removes.
CleanPrefix = []byte("ADD_RESULTS(")
// CleanSuffix is the suffix that CleanJSON removes.
CleanSuffix = []byte(");")
)
// CleanJSON returns the result of removing CleanPrefix
// and CleanSuffix from the contents in r. If either
// CleanPrefix or CleanSuffix does not exist, the returned
// io.Reader has the same contents as r.
func CleanJSON(r io.Reader) (io.Reader, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
if bytes.HasPrefix(b, CleanPrefix) && bytes.HasSuffix(b, CleanSuffix) {
result := bytes.TrimPrefix(b, CleanPrefix)
result = bytes.TrimSuffix(result, CleanSuffix)
return bytes.NewReader(result), nil
}
return bytes.NewReader(b), nil
}
// AggregateResult represents "results.json" and "results-small.json" files.
type AggregateResult struct {
Version int
Builder string
*BuilderInfo
}
// BuilderInfo represents aggregate information for a builder.
type BuilderInfo struct {
// SecondsEpoch is the start time of tests expressed in seconds from
// the Unix epoch.
SecondsEpoch []float64 `json:"secondsSinceEpoch"`
// BlinkRevs is list of Blink revisions.
BlinkRevs []Number `json:"blinkRevision"`
// BuildNumbers is list of build numbers.
BuildNumbers []Number `json:"buildNumbers"`
// ChromeRevs is a list of Chrome/Chromium revisions.
// The elements are strings because they can either be revision
// numbers or commit hashes.
ChromeRevs []string `json:"chromeRevision"`
// Tests is the test trie. The leaf nodes will be of type
// AggregateTestLeaf.
Tests AggregateTest `json:"tests"`
// FailureMap is a map from long failure types to short failure
// types. Usually, it resembles LongFailureTypes.
FailureMap map[string]string `json:"failure_map"`
// FailuresByType is a map from long failure type to
// number of failures.
FailuresByType map[string][]int `json:"num_failures_by_type,omitempty"`
// FixableCounts represents test failures in a legacy format,
// and is usually nil.
//
// It is included here because it may be needed to compute
// FailuresByType when unmarshaling JSON.
FixableCounts []map[string]int `json:"fixableCounts,omitempty"`
}
// TestList is a representation an AggregateResult in which
// the Results and Runtimes fields of all the AggregateTestLeafs
// are set to nil.
type TestList struct {
Builder string
Tests AggregateTest
}
// MarshalJSON marshals tl into JSON.
func (tl *TestList) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]map[string]AggregateTest{
tl.Builder: {
"tests": tl.Tests,
},
})
}
// ToTestList returns a TestList representation of ag.
// The receiver's Tests field will be modified in the process.
func (ag *AggregateResult) ToTestList() TestList {
tl := TestList{
Builder: ag.Builder,
Tests: ag.Tests,
}
tl.Tests.WalkLeaves(func(_ string, leaf *AggregateTestLeaf) {
leaf.Results = nil
leaf.Runtimes = nil
})
return tl
}
// MarshalJSON marshal ag into JSON.
func (ag *AggregateResult) MarshalJSON() ([]byte, error) {
v, err := json.Marshal(ag.Version)
if err != nil {
return nil, err
}
vRaw := json.RawMessage(v)
info := *ag.BuilderInfo
// If FailuresByType exists, do not include FixableCounts
// because it is deprecated.
if info.FailuresByType != nil {
info.FixableCounts = nil
}
b, err := json.Marshal(&info)
if err != nil {
return nil, err
}
infoRaw := json.RawMessage(b)
return json.Marshal(map[string]*json.RawMessage{
"version": &vRaw,
ag.Builder: &infoRaw,
})
}
// extractBuilderName gets the builder name from the supplied map.
// This depends on the fact that AggregateResults are expected to
// only have two top-level keys: (1) "version" (2) the builder name.
func extractBuilderName(m map[string]json.RawMessage) (string, error) {
for k := range m {
if k != "version" {
return k, nil
}
}
return "", errors.New("builder name not found")
}
// UnmarshalJSON decodes JSON data into t.
//
// The expected format is a modified version of the format described in the URL
// below. The modifications account for the structure of results.json and
// results_small.json files in the wild.
//
// https://chromium.googlesource.com/chromium/src/+/c7dd0560d9544a15908239bebc177410899851ca/third_party/WebKit/Tools/Scripts/webkitpy/layout_tests/layout_package/bot_test_expectations.py#45
func (ag *AggregateResult) UnmarshalJSON(data []byte) error {
var m map[string]json.RawMessage
if err := json.Unmarshal(data, &m); err != nil {
return err
}
// Version.
n, err := parseVersion(m)
if err != nil {
return err
}
ag.Version = n
// Builder name.
builder, err := extractBuilderName(m)
if err != nil {
return err
}
ag.Builder = builder
// BuilderInfo.
var info *BuilderInfo
if err := json.Unmarshal(m[builder], &info); err != nil {
return err
}
ag.BuilderInfo = info
if err := ag.checkFields(); err != nil {
return err
}
if err := info.computeFailuresByType(); err != nil {
return err
}
return info.checkFields()
}
func parseVersion(m map[string]json.RawMessage) (int, error) {
vBytes, ok := m["version"]
if !ok {
return 0, errors.New("model: missing key version")
}
vStr := string(vBytes)
n, err := strconv.Atoi(vStr)
if err != nil {
return 0, fmt.Errorf("model: version %q must be int: %v", vStr, err)
}
return n, nil
}
type fieldError struct {
Name string // Name of field.
Value interface{} // Invalid value in the field that caused error.
}
func (f *fieldError) Error() string {
return fmt.Sprintf("model: field %q has invalid value: %v (%T)", f.Name, f.Value, f.Value)
}
func (ag *AggregateResult) checkFields() error {
if ag.Version > ResultsVersion {
return &fieldError{"Version", ag.Version}
}
if ag.BuilderInfo == nil {
return &fieldError{"BuilderInfo", ag.BuilderInfo}
}
return nil
}
func (info *BuilderInfo) checkFields() error {
if info.BuildNumbers == nil {
return &fieldError{"BuildNumbers", info.BuildNumbers}
}
return nil
}
// computeFailuresByType computes info.FailuresByType from info.FixableCounts.
// The function has no effect if info.FailuresByType is already non-nil.
func (info *BuilderInfo) computeFailuresByType() error {
if info.FailuresByType != nil {
// Already present.
return nil
}
if info.FixableCounts == nil {
return errors.New("uploaded file is missing required num_failures_by_type field")
}
res := make(map[string][]int)
for _, fc := range info.FixableCounts {
for short, count := range fc {
long, ok := FailureLongNames[short]
if !ok {
return fmt.Errorf("model: unknown key %q", short)
}
res[long] = append(res[long], count)
}
}
info.FailuresByType = res
return nil
}
// AggregateTest represents Tests in a AggregateResult.
type AggregateTest map[string]Node
var _ Node = (AggregateTest)(nil)
func (at AggregateTest) node() {}
// Walk performs a depth-first traversal of the Nodes reachable
// from the receiver, calling fn each time. The Node in fn
// is guaranteed to be either AggregateTest or *AggregateTestLeaf.
// The traversal order may vary across different runs.
func (at AggregateTest) Walk(fn func(key string, node Node)) {
for key, node := range at {
switch val := node.(type) {
case *AggregateTestLeaf:
fn(key, val)
case AggregateTest:
fn(key, val)
val.Walk(fn)
}
}
}
// WalkLeaves is similar to Walk but only calls fn for
// *AggregateTestLeaf.
func (at AggregateTest) WalkLeaves(fn func(key string, leaf *AggregateTestLeaf)) {
at.Walk(func(key string, node Node) {
if leaf, ok := node.(*AggregateTestLeaf); ok {
fn(key, leaf)
}
})
}
// MarshalJSON marshals at into JSON.
func (at *AggregateTest) MarshalJSON() ([]byte, error) {
if at == nil {
return json.Marshal(nil)
}
m := make(map[string]*json.RawMessage)
for k, v := range *at {
b, err := json.Marshal(&v)
if err != nil {
return nil, err
}
raw := json.RawMessage(b)
m[k] = &raw
}
return json.Marshal(m)
}
// UnmarshalJSON unmarshals the supplied data into at.
func (at *AggregateTest) UnmarshalJSON(data []byte) error {
var m map[string]*json.RawMessage
if err := json.Unmarshal(data, &m); err != nil {
return err
}
if at == nil {
return errors.New("model: UnmarshalJSON: nil *AggregateTest")
}
if *at == nil {
*at = AggregateTest{}
}
return at.constructTree(m)
}
// constructTree constructs the tree of Nodes from the supplied map.
func (at *AggregateTest) constructTree(m map[string]*json.RawMessage) error {
for k, v := range m {
maybeLeaf := map[string]*json.RawMessage{}
err := json.Unmarshal(*v, &maybeLeaf)
if err != nil {
return err
}
if isAggregateTestLeaf(maybeLeaf) {
l := &AggregateTestLeaf{}
err := json.Unmarshal(*v, l)
if err != nil {
return err
}
if *at == nil {
*at = AggregateTest{}
}
(*at)[k] = l
continue
}
var child AggregateTest
if err := child.constructTree(maybeLeaf); err != nil {
return err
}
if *at == nil {
*at = AggregateTest{}
}
if child["artifacts"] != nil {
// Whelp, child is actually a AggregateTestLeaf pretending to be an AggregateTest
// because it was parsed incorrectly by bugs in older code. Hoist its contents
// up to the parent level. The actual artifacts field contents have been lost
// so we don't bother trying to reconstruct them.
actualLeaf := child["artifacts"].(*AggregateTestLeaf)
(*at)[k] = actualLeaf
continue
}
(*at)[k] = child
}
return nil
}
// isAggregateTestLeaf returns true if the supplied map is likely an
// AggregateTestLeaf.
func isAggregateTestLeaf(m map[string]*json.RawMessage) bool {
for key, val := range m {
if key == "results" && len(*val) > 0 {
return true
}
}
return false
}
// makeAggregateTestLeaf returns a AggregateTestLeaf from the supplied map.
func makeAggregateTestLeaf(m map[string]*json.RawMessage) (*AggregateTestLeaf, error) {
l := &AggregateTestLeaf{}
b, err := json.Marshal(m)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, l)
return l, err
}
// AggregateTestLeaf is the summary of test results at the l of a tests trie.
type AggregateTestLeaf struct {
Results []ResultSummary
Runtimes []RuntimeSummary
Expected []string
Bugs []string
}
func (leaf *AggregateTestLeaf) node() {}
// aggregateTestLeafAux is used to marshal and unmarshal AggregateTestLeaf.
type aggregateTestLeafAux struct {
Results []ResultSummary `json:"results,omitempty"`
Runtimes []RuntimeSummary `json:"times,omitempty"`
Expected *string `json:"expected,omitempty"`
Bugs []string `json:"bugs,omitempty"`
}
// MarshalJSON marshals leaf into JSON.
func (leaf *AggregateTestLeaf) MarshalJSON() ([]byte, error) {
aux := aggregateTestLeafAux{
Results: leaf.Results,
Runtimes: leaf.Runtimes,
Bugs: leaf.Bugs,
}
if s := strings.Join(leaf.Expected, " "); len(s) > 0 {
aux.Expected = &s
}
return json.Marshal(&aux)
}
// UnmarshalJSON unmarshals the supplied data into leaf.
func (leaf *AggregateTestLeaf) UnmarshalJSON(data []byte) error {
var aux aggregateTestLeafAux
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
leaf.Results = aux.Results
leaf.Runtimes = aux.Runtimes
if aux.Expected != nil {
leaf.Expected = strings.Split(*aux.Expected, " ")
}
leaf.Bugs = aux.Bugs
return nil
}
// defaultFields sets default values for missing/invalid fieldatastore.
func (leaf *AggregateTestLeaf) defaultFields() {
if len(leaf.Results) == 0 {
leaf.Results = []ResultSummary{{1, "N"}}
}
if len(leaf.Runtimes) == 0 {
leaf.Runtimes = []RuntimeSummary{{1, 0}}
}
}
// ResultSummary is the type of test failure and count of how many
// times the running time occurred.
type ResultSummary struct {
Count int
Type string
}
// MarshalJSON marshals rs into JSON.
func (rs *ResultSummary) MarshalJSON() ([]byte, error) {
return json.Marshal([]interface{}{
rs.Count,
rs.Type,
})
}
// UnmarshalJSON unmarshals the provided data into rs.
func (rs *ResultSummary) UnmarshalJSON(data []byte) error {
var tmp []*json.RawMessage
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
if len(tmp) != 2 {
return fmt.Errorf("model: UnmarshalJSON: ResultSummary wrong length: %d, expect: %d", len(tmp), 2)
}
count := float64(0)
if err := json.Unmarshal(*tmp[0], &count); err != nil {
return fmt.Errorf("model: UnmarshalJSON: ResultSummary wrong type: %v", tmp)
}
rs.Count = int(count)
if err := json.Unmarshal(*tmp[1], &rs.Type); err != nil {
return fmt.Errorf("model: UnmarshalJSON: ResultSummary wrong type: %v", tmp)
}
return nil
}
// RuntimeSummary is the running time of a test and count of how many
// times the running time occurred.
type RuntimeSummary struct {
Count int
Runtime float64
}
// MarshalJSON marshals rs into JSON.
func (rs *RuntimeSummary) MarshalJSON() ([]byte, error) {
return json.Marshal([]float64{
float64(rs.Count),
rs.Runtime,
})
}
// UnmarshalJSON unmarshals the provided data into rs.
func (rs *RuntimeSummary) UnmarshalJSON(data []byte) error {
var tmp []float64
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
if len(tmp) != 2 {
return fmt.Errorf("model: UnmarshalJSON: RuntimeSummary wrong length: %d, expect: %d", len(tmp), 2)
}
rs.Count = int(tmp[0])
rs.Runtime = tmp[1]
return nil
}
var (
// ErrBuildNumberConflict is returned when the build numbers
// are the same when merging.
ErrBuildNumberConflict = errors.New("build number conflict")
// ErrBuilderNameConflict is returned when the builder names
// do not match when merging.
ErrBuilderNameConflict = errors.New("builder name conflict")
)
// Merge merges other into ag.
func (ag *AggregateResult) Merge(other *AggregateResult) error {
if ag.Builder != other.Builder {
return ErrBuilderNameConflict
}
if ag.BuilderInfo == nil {
ag.BuilderInfo = &BuilderInfo{}
}
ag.Version = ResultsVersion
return ag.BuilderInfo.Merge(other.BuilderInfo)
}
// Merge merges other into info.
//
// The returned error is ErrBuildNumberConflict when
// other.BuildNumbers[0] already has the latest build number.
// This can happen if a single build re-uses a step name, or the
// normalized test name derived from the step name is the same as another
// step in the same build.
func (info *BuilderInfo) Merge(other *BuilderInfo) error {
if len(info.BuildNumbers) > 0 && len(other.BuildNumbers) > 0 {
if info.BuildNumbers[0] == other.BuildNumbers[0] {
return ErrBuildNumberConflict
}
}
info.SecondsEpoch = append(other.SecondsEpoch, info.SecondsEpoch...)
info.BlinkRevs = append(other.BlinkRevs, info.BlinkRevs...)
info.BuildNumbers = append(other.BuildNumbers, info.BuildNumbers...)
info.ChromeRevs = append(other.ChromeRevs, info.ChromeRevs...)
if info.FailuresByType == nil && other.FailuresByType != nil {
info.FailuresByType = make(map[string][]int)
}
for k, v := range other.FailuresByType {
info.FailuresByType[k] = append(v, info.FailuresByType[k]...)
}
info.FailureMap = FailureLongNames
if info.Tests == nil {
info.Tests = AggregateTest{}
}
info.Tests.WalkLeaves(func(_ string, leaf *AggregateTestLeaf) {
leaf.Expected = nil
leaf.Bugs = nil
})
return info.Tests.Merge(other.Tests)
}
// Merge merges other into at.
func (at *AggregateTest) Merge(other AggregateTest) error {
// Shallow copy but OK. We take care to not modify otherCopy
// values; instead always create new objects
// and assign to otherCopy[key].
otherCopy := make(AggregateTest, len(other))
for k, v := range other {
otherCopy[k] = v
}
for k, v := range *at {
if _, ok := otherCopy[k]; !ok {
switch v.(type) {
case *AggregateTestLeaf:
l := &AggregateTestLeaf{}
l.defaultFields()
otherCopy[k] = l
case AggregateTest:
otherCopy[k] = AggregateTest{}
}
}
}
for k, v := range otherCopy {
// Key does not exist: assign entire subtree.
if _, ok := (*at)[k]; !ok {
if *at == nil {
*at = AggregateTest{}
}
(*at)[k] = v
continue
}
// Leaf node.
if leaf1, ok := (*at)[k].(*AggregateTestLeaf); ok {
leaf2, ok := v.(*AggregateTestLeaf)
if !ok {
return fmt.Errorf("model: Merge: %q expected *AggregateTestLeaf, but got: %#v", k, v)
}
if err := leaf1.Merge(leaf2); err != nil {
return err
}
continue
}
// Not leaf node: merge subtree recursively.
at1, ok := (*at)[k].(AggregateTest)
if !ok {
return fmt.Errorf("model: Merge (*at)[%s]: expected AggregateTest, but got: %#v", k, (*at)[k])
}
at2, ok := v.(AggregateTest)
if !ok {
return fmt.Errorf("model: Merge (v) %q: expected AggregateTest, but got: %#v", k, v)
}
if err := at1.Merge(at2); err != nil {
return err
}
}
return nil
}
// Merge merges other into leaf.
func (leaf *AggregateTestLeaf) Merge(other *AggregateTestLeaf) error {
// Bugs and Expected should come from from other only.
leaf.Bugs = other.Bugs
if len(other.Expected) == 1 && other.Expected[0] != "PASS" {
leaf.Expected = other.Expected
}
for _, r := range other.Results {
if len(leaf.Results) > 0 && r.Type == leaf.Results[0].Type {
leaf.Results[0].Count += r.Count
} else {
leaf.Results = append([]ResultSummary{r}, leaf.Results...)
}
}
for _, r := range other.Runtimes {
if len(leaf.Runtimes) > 0 && r.Runtime == leaf.Runtimes[0].Runtime {
leaf.Runtimes[0].Count += r.Count
} else {
leaf.Runtimes = append([]RuntimeSummary{r}, leaf.Runtimes...)
}
}
return nil
}
const (
// ResultsSize is the size that "results.json" should be trimmed to.
ResultsSize = 500
// ResultsSmallSize is the size that "results_small.json" should
// be trimmed to.
ResultsSmallSize = 100
runtimeThresholdNormal float64 = 3 // In secondatastore.
runtimeThresholdDebug float64 = 9 // In secondatastore.
)
func isDebugBuilder(builder string) bool {
for _, s := range []string{"debug", "dbg"} {
if strings.Contains(strings.ToLower(builder), s) {
return true
}
}
return false
}
// Trim trims ag's fields to the specified size.
func (ag *AggregateResult) Trim(size int) error {
t := runtimeThresholdNormal
if isDebugBuilder(ag.Builder) {
t = runtimeThresholdDebug
}
ag.SecondsEpoch = ag.SecondsEpoch[:min(size, len(ag.SecondsEpoch))]
ag.BlinkRevs = ag.BlinkRevs[:min(size, len(ag.BlinkRevs))]
ag.ChromeRevs = ag.ChromeRevs[:min(size, len(ag.ChromeRevs))]
ag.BuildNumbers = ag.BuildNumbers[:min(size, len(ag.BuildNumbers))]
return ag.Tests.trim(size, t)
}
func (at AggregateTest) trim(size int, threshold float64) error {
for k, v := range at {
if leaf, ok := v.(*AggregateTestLeaf); ok {
leaf.trim(size)
if leaf.shouldDelete(threshold) {
delete(at, k)
}
continue
}
child, ok := v.(AggregateTest)
if !ok {
return errors.New("model: trim: expected AggregateTest")
}
if err := child.trim(size, threshold); err != nil {
return err
}
if len(child) == 0 {
delete(at, k)
}
}
return nil
}
func (leaf *AggregateTestLeaf) trim(size int) {
n := 0
for i, r := range leaf.Results {
leaf.Results[i].Count = min(r.Count, size)
n += r.Count
if n >= size {
leaf.Results = leaf.Results[:i+1]
break
}
}
n = 0
for i, r := range leaf.Runtimes {
leaf.Runtimes[i].Count = min(r.Count, size)
n += r.Count
if n >= size {
leaf.Runtimes = leaf.Runtimes[:i+1]
break
}
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
var deletableTypes = map[string]bool{"P": true, "N": true, "Y": true}
func (leaf *AggregateTestLeaf) shouldDelete(threshold float64) bool {
if len(leaf.Expected) == 1 && leaf.Expected[0] != "PASS" {
return false
}
if leaf.Bugs != nil {
return false
}
for _, r := range leaf.Results {
if !deletableTypes[r.Type] {
return false
}
}
for _, r := range leaf.Runtimes {
if r.Runtime >= threshold {
return false
}
}
return true
}