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 ¤t 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 }