| Index: go/src/infra/appengine/test-results/model/aggregate_result.go
|
| diff --git a/go/src/infra/appengine/test-results/model/aggregate_result.go b/go/src/infra/appengine/test-results/model/aggregate_result.go
|
| index 4eccfaaac90859bcd90f08e81e1efd23eb2bf82d..7461629ed1a9c8c555e37540e2ebb173c9fdcd43 100644
|
| --- a/go/src/infra/appengine/test-results/model/aggregate_result.go
|
| +++ b/go/src/infra/appengine/test-results/model/aggregate_result.go
|
| @@ -366,7 +366,7 @@ type AggregateTestLeaf struct {
|
| Bugs []string
|
| }
|
|
|
| -func (l *AggregateTestLeaf) node() {}
|
| +func (leaf *AggregateTestLeaf) node() {}
|
|
|
| // aggregateTestLeafAux is used to marshal and unmarshal AggregateTestLeaf.
|
| type aggregateTestLeafAux struct {
|
| @@ -377,42 +377,42 @@ type aggregateTestLeafAux struct {
|
| }
|
|
|
| // MarshalJSON marshal l into JSON.
|
| -func (l *AggregateTestLeaf) MarshalJSON() ([]byte, error) {
|
| +func (leaf *AggregateTestLeaf) MarshalJSON() ([]byte, error) {
|
| aux := aggregateTestLeafAux{
|
| - Results: l.Results,
|
| - Runtimes: l.Runtimes,
|
| - Bugs: l.Bugs,
|
| + Results: leaf.Results,
|
| + Runtimes: leaf.Runtimes,
|
| + Bugs: leaf.Bugs,
|
| }
|
| - if s := strings.Join(l.Expected, " "); len(s) > 0 {
|
| + if s := strings.Join(leaf.Expected, " "); len(s) > 0 {
|
| aux.Expected = &s
|
| }
|
| return json.Marshal(&aux)
|
| }
|
|
|
| // UnmarshalJSON unmarshal the supplied data into l.
|
| -func (l *AggregateTestLeaf) UnmarshalJSON(data []byte) error {
|
| +func (leaf *AggregateTestLeaf) UnmarshalJSON(data []byte) error {
|
| var aux aggregateTestLeafAux
|
| if err := json.Unmarshal(data, &aux); err != nil {
|
| return err
|
| }
|
|
|
| - l.Results = aux.Results
|
| - l.Runtimes = aux.Runtimes
|
| + leaf.Results = aux.Results
|
| + leaf.Runtimes = aux.Runtimes
|
| if aux.Expected != nil {
|
| - l.Expected = strings.Split(*aux.Expected, " ")
|
| + leaf.Expected = strings.Split(*aux.Expected, " ")
|
| }
|
| - l.Bugs = aux.Bugs
|
| + leaf.Bugs = aux.Bugs
|
|
|
| return nil
|
| }
|
|
|
| // defaultFields sets default values for missing/invalid fields.
|
| -func (l *AggregateTestLeaf) defaultFields() {
|
| - if len(l.Results) == 0 {
|
| - l.Results = []ResultSummary{{1, "N"}}
|
| +func (leaf *AggregateTestLeaf) defaultFields() {
|
| + if len(leaf.Results) == 0 {
|
| + leaf.Results = []ResultSummary{{1, "N"}}
|
| }
|
| - if len(l.Runtimes) == 0 {
|
| - l.Runtimes = []RuntimeSummary{{1, 0}}
|
| + if len(leaf.Runtimes) == 0 {
|
| + leaf.Runtimes = []RuntimeSummary{{1, 0}}
|
| }
|
| }
|
|
|
| @@ -484,3 +484,262 @@ func (rs *RuntimeSummary) UnmarshalJSON(data []byte) error {
|
| 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.
|
| +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 errors.New("model: Merge: expected *AggregateTestLeaf")
|
| + }
|
| + if err := leaf1.Merge(leaf2); err != nil {
|
| + return err
|
| + }
|
| + continue
|
| + }
|
| +
|
| + // Not leaf node: merge subtree recursively.
|
| + at1, ok := (*at)[k].(AggregateTest)
|
| + if !ok {
|
| + return errors.New("model: Merge: expected AggregateTest")
|
| + }
|
| + at2, ok := v.(AggregateTest)
|
| + if !ok {
|
| + return errors.New("model: Merge: expected AggregateTest")
|
| + }
|
| + 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 seconds.
|
| + runtimeThresholdDebug float64 = 9 // In seconds.
|
| +)
|
| +
|
| +func isDebugBuilder(builder string) bool {
|
| + for _, s := range []string{"debug", "dbg"} {
|
| + if strings.Contains(strings.ToLower(builder), s) {
|
| + return true
|
| + }
|
| + }
|
| + return false
|
| +}
|
| +
|
| +// Trim trims the leaves of Tests in ar to the specified size.
|
| +func (ag *AggregateResult) Trim(size int) error {
|
| + t := runtimeThresholdNormal
|
| +
|
| + if isDebugBuilder(ag.Builder) {
|
| + t = runtimeThresholdDebug
|
| + }
|
| +
|
| + 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
|
| +}
|
|
|