github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/inflate.go (about)

     1  /*
     2  Copyright 2020 The TestGrid Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package updater
    18  
    19  import (
    20  	"context"
    21  	"time"
    22  
    23  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    24  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    25  )
    26  
    27  // InflatedColumn holds all the entries for a given column.
    28  //
    29  // This includes both:
    30  // * Column state metadata and
    31  // * Cell values for every row in this column
    32  type InflatedColumn struct {
    33  	// Column holds the header data.
    34  	Column *statepb.Column
    35  	// Cells holds each row's uncompressed data for this column.
    36  	Cells map[string]Cell // TODO(fejta): *Cell
    37  }
    38  
    39  // Cell holds a row's values for a given column
    40  type Cell struct {
    41  	// Result determines the color of the cell, defaulting to NO_RESULT (clear)
    42  	Result statuspb.TestStatus
    43  
    44  	// The name of the row before user-customized formatting
    45  	ID string
    46  
    47  	// CellID specifies the an identifier to the build, which allows
    48  	// clicking different cells in a column to go to different locations.
    49  	CellID string
    50  
    51  	// Properties maps key:value pairs for cell IDs.
    52  	Properties map[string]string
    53  
    54  	// Icon is a short string that appears on the cell
    55  	Icon string
    56  	// Message is a longer string that appears on mouse-over
    57  	Message string
    58  
    59  	// Metrics holds numerical data, such as how long it ran, coverage, etc.
    60  	Metrics map[string]float64
    61  
    62  	// UserProperty holds the value of a user-defined property, which allows
    63  	// runtime flexibility in generating links to click on.
    64  	UserProperty string
    65  
    66  	// Issues relevant to this cell
    67  	// TODO(fejta): persist cell association, currently gets written out as a row-association.
    68  	// TODO(fejta): support issue association when parsing prow job results.
    69  	Issues []string
    70  }
    71  
    72  // InflateGrid inflates the grid's rows into an InflatedColumn channel.
    73  //
    74  // Drops columns before earliest or more recent than latest.
    75  // Also returns a map of issues associated with each row name.
    76  func InflateGrid(ctx context.Context, grid *statepb.Grid, earliest, latest time.Time) ([]InflatedColumn, map[string][]string, error) {
    77  	var cols []InflatedColumn
    78  	if n := len(grid.Columns); n > 0 {
    79  		cols = make([]InflatedColumn, 0, n)
    80  	}
    81  
    82  	ctx, cancel := context.WithCancel(ctx)
    83  	defer cancel()
    84  
    85  	rows := make(map[string]func() *Cell, len(grid.Rows))
    86  	issues := make(map[string][]string, len(grid.Rows))
    87  	for _, row := range grid.Rows {
    88  		rows[row.Name] = inflateRow(row)
    89  		if len(row.Issues) > 0 {
    90  			issues[row.Name] = row.Issues
    91  		}
    92  	}
    93  
    94  	for _, col := range grid.Columns {
    95  		if err := ctx.Err(); err != nil {
    96  			return nil, nil, err
    97  		}
    98  		// Even if we wind up skipping the column
    99  		// we still need to inflate the cells.
   100  		item := InflatedColumn{
   101  			Column: col,
   102  			Cells:  make(map[string]Cell, len(rows)),
   103  		}
   104  		if col.Hint == "" { // TODO(fejta): drop after everything sets its hint.
   105  			col.Hint = col.Build
   106  		}
   107  		for rowName, nextCell := range rows {
   108  			cell := nextCell()
   109  			if cell != nil {
   110  				item.Cells[rowName] = *cell
   111  			}
   112  		}
   113  		when := int64(col.Started / 1000)
   114  		if when > latest.Unix() {
   115  			continue
   116  		}
   117  		if when < earliest.Unix() && len(cols) > 0 { // Always keep at least one old column
   118  			continue // Do not assume they are sorted by start time.
   119  		}
   120  		cols = append(cols, item)
   121  	}
   122  	return cols, issues, nil
   123  }
   124  
   125  // inflateRow inflates the values for each column into a Cell channel.
   126  func inflateRow(row *statepb.Row) func() *Cell {
   127  	if row == nil {
   128  		return func() *Cell { return nil }
   129  	}
   130  	addCellID := hasCellID(row.Name)
   131  
   132  	var filledIdx int
   133  	var mets map[string]func() (*float64, bool)
   134  	if len(row.Metrics) > 0 {
   135  		mets = make(map[string]func() (*float64, bool), len(row.Metrics))
   136  	}
   137  	for i, m := range row.Metrics {
   138  		if m.Name == "" && len(row.Metrics) > i {
   139  			m.Name = row.Metric[i]
   140  		}
   141  		mets[m.Name] = inflateMetric(m)
   142  	}
   143  	var val *float64
   144  	nextResult := inflateResults(row.Results)
   145  	return func() *Cell {
   146  		for cur := nextResult(); cur != nil; cur = nextResult() {
   147  			result := *cur
   148  			c := Cell{
   149  				Result: result,
   150  				ID:     row.Id,
   151  			}
   152  			for name, nextValue := range mets {
   153  				val, _ = nextValue()
   154  				if val == nil {
   155  					continue
   156  				}
   157  				if c.Metrics == nil {
   158  					c.Metrics = make(map[string]float64, 2)
   159  				}
   160  				c.Metrics[name] = *val
   161  			}
   162  			// TODO(fejta): consider returning (nil, true) instead here
   163  			if result != statuspb.TestStatus_NO_RESULT {
   164  				c.Icon = row.Icons[filledIdx]
   165  				c.Message = row.Messages[filledIdx]
   166  				if addCellID {
   167  					c.CellID = row.CellIds[filledIdx]
   168  				}
   169  				if len(row.Properties) != 0 && c.Properties == nil {
   170  					c.Properties = make(map[string]string)
   171  				}
   172  				if filledIdx < len(row.Properties) {
   173  					for k, v := range row.GetProperties()[filledIdx].GetProperty() {
   174  						c.Properties[k] = v
   175  					}
   176  				}
   177  				if n := len(row.UserProperty); n > filledIdx {
   178  					c.UserProperty = row.UserProperty[filledIdx]
   179  				}
   180  				filledIdx++
   181  			}
   182  			return &c
   183  		}
   184  		return nil
   185  	}
   186  }
   187  
   188  // inflateMetric inflates the sparse-encoded metric values into a channel
   189  //
   190  // {Indices: [0,2,6,4], Values: {0.1, 0.2, 6.1, 6.2, 6.3, 6.4}} encodes:
   191  // {0.1, 0.2, nil, nil, nil, nil, 6.1, 6.2, 6.3, 6.4}
   192  func inflateMetric(metric *statepb.Metric) func() (*float64, bool) {
   193  	idx := -1
   194  	var remain int32
   195  	valueIdx := -1
   196  	current := int32(-1)
   197  	var start int32
   198  	more := true
   199  	return func() (*float64, bool) {
   200  		if !more {
   201  			return nil, false
   202  		}
   203  		for {
   204  			if remain > 0 {
   205  				current++
   206  				if current < start {
   207  					return nil, true
   208  				}
   209  				remain--
   210  				valueIdx++
   211  				if valueIdx == len(metric.Values) {
   212  					break
   213  				}
   214  				v := metric.Values[valueIdx]
   215  				return &v, true
   216  			}
   217  			idx++
   218  			if idx >= len(metric.Indices)-1 {
   219  				break
   220  			}
   221  			start = metric.Indices[idx]
   222  			idx++
   223  			remain = metric.Indices[idx]
   224  		}
   225  		more = false
   226  		return nil, false
   227  	}
   228  }
   229  
   230  // inflateResults inflates the run-length encoded row results into a channel.
   231  //
   232  // [PASS, 2, NO_RESULT, 1, FAIL, 3] is equivalent to:
   233  // [PASS, PASS, NO_RESULT, FAIL, FAIL, FAIL]
   234  func inflateResults(results []int32) func() *statuspb.TestStatus {
   235  	idx := -1
   236  	var current statuspb.TestStatus
   237  	var remain int32
   238  	more := true
   239  	return func() *statuspb.TestStatus {
   240  		if !more {
   241  			return nil
   242  		}
   243  		for {
   244  			if remain > 0 {
   245  				remain--
   246  				return &current
   247  			}
   248  			idx++
   249  			if idx == len(results) {
   250  				break
   251  			}
   252  			current = statuspb.TestStatus(results[idx])
   253  			idx++
   254  			remain = results[idx]
   255  		}
   256  		more = false
   257  		return nil
   258  	}
   259  }