github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/inflate_test.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  	"flag"
    22  	"reflect"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-cmp/cmp"
    27  	"google.golang.org/protobuf/testing/protocmp"
    28  
    29  	"cloud.google.com/go/storage"
    30  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    31  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    32  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    33  )
    34  
    35  // TODO(fejta): rename everything to InflatedColumn
    36  type inflatedColumn = InflatedColumn
    37  
    38  // TODO(fejta): rename everything to Cell
    39  type cell = Cell
    40  
    41  func blank(n int) []string {
    42  	var out []string
    43  	for i := 0; i < n; i++ {
    44  		out = append(out, "")
    45  	}
    46  	return out
    47  }
    48  
    49  var benchPath gcs.Path
    50  
    51  func init() {
    52  	flag.Var(&benchPath, "bench-path", "Path to ./foo/local-state or gs://bucket/grid/test-group")
    53  }
    54  
    55  func BenchmarkInflateGrid(b *testing.B) {
    56  	ctx := context.Background()
    57  	if benchPath.Object() == "" {
    58  		b.Skip("No grid-path specified")
    59  	}
    60  	var storageClient *storage.Client
    61  	if benchPath.Bucket() != "" {
    62  		s, err := gcs.ClientWithCreds(context.Background())
    63  		if err != nil {
    64  			b.Fatalf("Cannot create GCS client: %v", err)
    65  		}
    66  		storageClient = s
    67  	}
    68  	client := gcs.NewClient(storageClient)
    69  	grid, _, err := gcs.DownloadGrid(ctx, client, benchPath)
    70  	if err != nil {
    71  		b.Fatalf("Failed to download %s: %v", benchPath, err)
    72  	}
    73  	latest := time.Now().Add(time.Hour)
    74  	earliest := time.Unix(0, 0)
    75  	b.ResetTimer()
    76  	b.RunParallel(func(pb *testing.PB) {
    77  		for pb.Next() {
    78  			InflateGrid(ctx, grid, earliest, latest)
    79  		}
    80  	})
    81  }
    82  
    83  func TestInflateGrid(t *testing.T) {
    84  	var hours []time.Time
    85  	when := time.Now().Round(time.Hour)
    86  	for i := 0; i < 24; i++ {
    87  		hours = append(hours, when)
    88  		when = when.Add(time.Hour)
    89  	}
    90  
    91  	millis := func(t time.Time) float64 {
    92  		return float64(t.Unix() * 1000)
    93  	}
    94  
    95  	cases := []struct {
    96  		name       string
    97  		ctx        context.Context
    98  		grid       *statepb.Grid
    99  		earliest   time.Time
   100  		latest     time.Time
   101  		expected   []inflatedColumn
   102  		wantIssues map[string][]string
   103  		err        bool
   104  	}{
   105  		{
   106  			name: "basically works",
   107  			grid: &statepb.Grid{},
   108  		},
   109  		{
   110  			name: "preserve column data",
   111  			ctx: func() context.Context {
   112  				ctx, cancel := context.WithCancel(context.Background())
   113  				cancel()
   114  				return ctx
   115  			}(),
   116  			grid: &statepb.Grid{
   117  				Columns: []*statepb.Column{
   118  					{
   119  						Build:      "build",
   120  						Hint:       "xyzpdq",
   121  						Name:       "name",
   122  						Started:    5,
   123  						Extra:      []string{"extra", "fun"},
   124  						HotlistIds: "hot topic",
   125  					},
   126  					{
   127  						Build:      "second build", // Also becomes Hint
   128  						Name:       "second name",
   129  						Started:    10,
   130  						Extra:      []string{"more", "gooder"},
   131  						HotlistIds: "hot pocket",
   132  					},
   133  				},
   134  			},
   135  			err: true,
   136  		},
   137  		{
   138  			name: "preserve column data",
   139  			grid: &statepb.Grid{
   140  				Columns: []*statepb.Column{
   141  					{
   142  						Build:      "build",
   143  						Hint:       "xyzpdq",
   144  						Name:       "name",
   145  						Started:    5,
   146  						Extra:      []string{"extra", "fun"},
   147  						HotlistIds: "hot topic",
   148  					},
   149  					{
   150  						Build:      "second build", // Also becomes Hint
   151  						Name:       "second name",
   152  						Started:    10,
   153  						Extra:      []string{"more", "gooder"},
   154  						HotlistIds: "hot pocket",
   155  					},
   156  				},
   157  			},
   158  			latest: hours[23],
   159  			expected: []inflatedColumn{
   160  				{
   161  					Column: &statepb.Column{
   162  						Build:      "build",
   163  						Hint:       "xyzpdq",
   164  						Name:       "name",
   165  						Started:    5,
   166  						Extra:      []string{"extra", "fun"},
   167  						HotlistIds: "hot topic",
   168  					},
   169  					Cells: map[string]cell{},
   170  				},
   171  				{
   172  					Column: &statepb.Column{
   173  						Build:      "second build",
   174  						Hint:       "second build",
   175  						Name:       "second name",
   176  						Started:    10,
   177  						Extra:      []string{"more", "gooder"},
   178  						HotlistIds: "hot pocket",
   179  					},
   180  					Cells: map[string]cell{},
   181  				},
   182  			},
   183  		},
   184  		{
   185  			name: "preserve row data",
   186  			grid: &statepb.Grid{
   187  				Columns: []*statepb.Column{
   188  					{
   189  						Build:   "b1",
   190  						Name:    "n1",
   191  						Started: 1,
   192  					},
   193  					{
   194  						Build:   "b2",
   195  						Name:    "n2",
   196  						Started: 2,
   197  					},
   198  				},
   199  				Rows: []*statepb.Row{
   200  					{
   201  						Name: "name",
   202  						Results: []int32{
   203  							int32(statuspb.TestStatus_FAIL), 2,
   204  						},
   205  						CellIds:      []string{"this", "that"},
   206  						Messages:     []string{"important", "notice"},
   207  						Icons:        []string{"I1", "I2"},
   208  						Metric:       []string{"this", "that"},
   209  						UserProperty: []string{"hello", "there"},
   210  						Metrics: []*statepb.Metric{
   211  							{
   212  								Indices: []int32{0, 2}, // both columns
   213  								Values:  []float64{0.1, 0.2},
   214  							},
   215  							{
   216  								Name:    "override",
   217  								Indices: []int32{1, 1}, // only second
   218  								Values:  []float64{1.1},
   219  							},
   220  						},
   221  						Issues: []string{"fun", "times"},
   222  					},
   223  					{
   224  						Name: "second",
   225  						Results: []int32{
   226  							int32(statuspb.TestStatus_PASS), 2,
   227  						},
   228  						CellIds:      blank(2),
   229  						Messages:     blank(2),
   230  						Icons:        blank(2),
   231  						Metric:       blank(2),
   232  						UserProperty: blank(2),
   233  					},
   234  					{
   235  						Name: "sparse",
   236  						Results: []int32{
   237  							int32(statuspb.TestStatus_NO_RESULT), 1,
   238  							int32(statuspb.TestStatus_FLAKY), 1,
   239  						},
   240  						CellIds:      []string{"that-sparse"},
   241  						Messages:     []string{"notice-sparse"},
   242  						Icons:        []string{"I2-sparse"},
   243  						UserProperty: []string{"there-sparse"},
   244  					},
   245  					{
   246  						Name:   "issued",
   247  						Issues: []string{"three", "4"},
   248  					},
   249  				},
   250  			},
   251  			latest: hours[23],
   252  			expected: []inflatedColumn{
   253  				{
   254  					Column: &statepb.Column{
   255  						Build:   "b1",
   256  						Hint:    "b1",
   257  						Name:    "n1",
   258  						Started: 1,
   259  					},
   260  					Cells: map[string]cell{
   261  						"name": {
   262  							Result:  statuspb.TestStatus_FAIL,
   263  							CellID:  "this",
   264  							Message: "important",
   265  							Icon:    "I1",
   266  							Metrics: map[string]float64{
   267  								"this": 0.1,
   268  							},
   269  							UserProperty: "hello",
   270  						},
   271  						"second": {
   272  							Result: statuspb.TestStatus_PASS,
   273  						},
   274  						"sparse": {},
   275  					},
   276  				},
   277  				{
   278  					Column: &statepb.Column{
   279  						Build:   "b2",
   280  						Hint:    "b2",
   281  						Name:    "n2",
   282  						Started: 2,
   283  					},
   284  					Cells: map[string]cell{
   285  						"name": {
   286  							Result:  statuspb.TestStatus_FAIL,
   287  							CellID:  "that",
   288  							Message: "notice",
   289  							Icon:    "I2",
   290  							Metrics: map[string]float64{
   291  								"this":     0.2,
   292  								"override": 1.1,
   293  							},
   294  							UserProperty: "there",
   295  						},
   296  						"second": {
   297  							Result: statuspb.TestStatus_PASS,
   298  						},
   299  						"sparse": {
   300  							Result:       statuspb.TestStatus_FLAKY,
   301  							CellID:       "that-sparse",
   302  							Message:      "notice-sparse",
   303  							Icon:         "I2-sparse",
   304  							UserProperty: "there-sparse",
   305  						},
   306  					},
   307  				},
   308  			},
   309  			wantIssues: map[string][]string{
   310  				"issued": {"three", "4"},
   311  				"name":   {"fun", "times"},
   312  			},
   313  		},
   314  		{
   315  			name: "drop latest columns",
   316  			grid: &statepb.Grid{
   317  				Columns: []*statepb.Column{
   318  					{
   319  						Build:   "latest1",
   320  						Started: millis(hours[23]),
   321  					},
   322  					{
   323  						Build:   "latest2",
   324  						Started: millis(hours[20]) + 1000,
   325  					},
   326  					{
   327  						Build:   "keep1",
   328  						Started: millis(hours[20]) + 999,
   329  					},
   330  					{
   331  						Build:   "keep2",
   332  						Started: millis(hours[10]),
   333  					},
   334  				},
   335  				Rows: []*statepb.Row{
   336  					{
   337  						Name:     "hello",
   338  						CellIds:  blank(4),
   339  						Messages: blank(4),
   340  						Icons:    blank(4),
   341  						Results: []int32{
   342  							int32(statuspb.TestStatus_RUNNING), 1,
   343  							int32(statuspb.TestStatus_PASS), 1,
   344  							int32(statuspb.TestStatus_FAIL), 1,
   345  							int32(statuspb.TestStatus_FLAKY), 1,
   346  						},
   347  					},
   348  					{
   349  						Name:     "world",
   350  						CellIds:  blank(4),
   351  						Messages: blank(4),
   352  						Icons:    blank(4),
   353  						Results: []int32{
   354  							int32(statuspb.TestStatus_PASS_WITH_SKIPS), 4,
   355  						},
   356  					},
   357  				},
   358  			},
   359  			latest: hours[20],
   360  			expected: []inflatedColumn{
   361  				{
   362  					Column: &statepb.Column{
   363  						Build:   "keep1",
   364  						Hint:    "keep1",
   365  						Started: millis(hours[20]) + 999,
   366  					},
   367  					Cells: map[string]cell{
   368  						"hello": {Result: statuspb.TestStatus_FAIL},
   369  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   370  					},
   371  				},
   372  				{
   373  					Column: &statepb.Column{
   374  						Build:   "keep2",
   375  						Hint:    "keep2",
   376  						Started: millis(hours[10]),
   377  					},
   378  					Cells: map[string]cell{
   379  						"hello": {Result: statuspb.TestStatus_FLAKY},
   380  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   381  					},
   382  				},
   383  			},
   384  		},
   385  		{
   386  			name: "unsorted", // drop old and new
   387  			grid: &statepb.Grid{
   388  				Columns: []*statepb.Column{
   389  					{
   390  						Build:   "current1",
   391  						Started: millis(hours[20]),
   392  					},
   393  					{
   394  						Build:   "old1",
   395  						Started: millis(hours[10]) - 1,
   396  					},
   397  					{
   398  						Build:   "new1",
   399  						Started: millis(hours[22]),
   400  					},
   401  					{
   402  						Build:   "current3",
   403  						Started: millis(hours[19]),
   404  					},
   405  					{
   406  						Build:   "new2",
   407  						Started: millis(hours[23]),
   408  					},
   409  					{
   410  						Build:   "old2",
   411  						Started: millis(hours[0]),
   412  					},
   413  					{
   414  						Build:   "current2",
   415  						Started: millis(hours[10]),
   416  					},
   417  				},
   418  				Rows: []*statepb.Row{
   419  					{
   420  						Name:     "hello",
   421  						CellIds:  blank(7),
   422  						Messages: blank(7),
   423  						Icons:    blank(7),
   424  						Results: []int32{
   425  							int32(statuspb.TestStatus_RUNNING), 1,
   426  							int32(statuspb.TestStatus_PASS), 2,
   427  							int32(statuspb.TestStatus_FAIL), 1,
   428  							int32(statuspb.TestStatus_PASS), 2,
   429  							int32(statuspb.TestStatus_FLAKY), 1,
   430  						},
   431  					},
   432  					{
   433  						Name:     "world",
   434  						CellIds:  blank(7),
   435  						Messages: blank(7),
   436  						Icons:    blank(7),
   437  						Results: []int32{
   438  							int32(statuspb.TestStatus_PASS_WITH_SKIPS), 7,
   439  						},
   440  					},
   441  				},
   442  			},
   443  			latest:   hours[21],
   444  			earliest: hours[10],
   445  			expected: []inflatedColumn{
   446  				{
   447  					Column: &statepb.Column{
   448  						Build:   "current1",
   449  						Hint:    "current1",
   450  						Started: millis(hours[20]),
   451  					},
   452  					Cells: map[string]cell{
   453  						"hello": {Result: statuspb.TestStatus_RUNNING},
   454  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   455  					},
   456  				},
   457  				{
   458  					Column: &statepb.Column{
   459  						Build:   "current3",
   460  						Hint:    "current3",
   461  						Started: millis(hours[19]),
   462  					},
   463  					Cells: map[string]cell{
   464  						"hello": {Result: statuspb.TestStatus_FAIL},
   465  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   466  					},
   467  				},
   468  				{
   469  					Column: &statepb.Column{
   470  						Build:   "current2",
   471  						Hint:    "current2",
   472  						Started: millis(hours[10]),
   473  					},
   474  					Cells: map[string]cell{
   475  						"hello": {Result: statuspb.TestStatus_FLAKY},
   476  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   477  					},
   478  				},
   479  			},
   480  		},
   481  		{
   482  			name: "drop old columns",
   483  			grid: &statepb.Grid{
   484  				Columns: []*statepb.Column{
   485  					{
   486  						Build:   "current1",
   487  						Started: millis(hours[20]),
   488  					},
   489  					{
   490  						Build:   "current2",
   491  						Started: millis(hours[10]),
   492  					},
   493  					{
   494  						Build:   "old1",
   495  						Started: millis(hours[10]) - 1,
   496  					},
   497  					{
   498  						Build:   "old2",
   499  						Started: millis(hours[0]),
   500  					},
   501  				},
   502  				Rows: []*statepb.Row{
   503  					{
   504  						Name:     "hello",
   505  						CellIds:  blank(4),
   506  						Messages: blank(4),
   507  						Icons:    blank(4),
   508  						Results: []int32{
   509  							int32(statuspb.TestStatus_RUNNING), 1,
   510  							int32(statuspb.TestStatus_PASS), 1,
   511  							int32(statuspb.TestStatus_FAIL), 1,
   512  							int32(statuspb.TestStatus_FLAKY), 1,
   513  						},
   514  					},
   515  					{
   516  						Name:     "world",
   517  						CellIds:  blank(4),
   518  						Messages: blank(4),
   519  						Icons:    blank(4),
   520  						Results: []int32{
   521  							int32(statuspb.TestStatus_PASS_WITH_SKIPS), 4,
   522  						},
   523  					},
   524  				},
   525  			},
   526  			latest:   hours[23],
   527  			earliest: hours[10],
   528  			expected: []inflatedColumn{
   529  				{
   530  					Column: &statepb.Column{
   531  						Build:   "current1",
   532  						Hint:    "current1",
   533  						Started: millis(hours[20]),
   534  					},
   535  					Cells: map[string]cell{
   536  						"hello": {Result: statuspb.TestStatus_RUNNING},
   537  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   538  					},
   539  				},
   540  				{
   541  					Column: &statepb.Column{
   542  						Build:   "current2",
   543  						Hint:    "current2",
   544  						Started: millis(hours[10]),
   545  					},
   546  					Cells: map[string]cell{
   547  						"hello": {Result: statuspb.TestStatus_PASS},
   548  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   549  					},
   550  				},
   551  			},
   552  		},
   553  		{
   554  			name: "keep newest old column when none newer",
   555  			grid: &statepb.Grid{
   556  				Columns: []*statepb.Column{
   557  					{
   558  						Build:   "drop-latest1",
   559  						Started: millis(hours[23]),
   560  					},
   561  					{
   562  						Build:   "keep-old1",
   563  						Started: millis(hours[10]) - 1,
   564  					},
   565  					{
   566  						Build:   "drop-old2",
   567  						Started: millis(hours[0]),
   568  					},
   569  				},
   570  				Rows: []*statepb.Row{
   571  					{
   572  						Name:     "hello",
   573  						CellIds:  blank(4),
   574  						Messages: blank(4),
   575  						Icons:    blank(4),
   576  						Results: []int32{
   577  							int32(statuspb.TestStatus_RUNNING), 1,
   578  							int32(statuspb.TestStatus_FAIL), 1,
   579  							int32(statuspb.TestStatus_FLAKY), 1,
   580  						},
   581  					},
   582  					{
   583  						Name:     "world",
   584  						CellIds:  blank(4),
   585  						Messages: blank(4),
   586  						Icons:    blank(4),
   587  						Results: []int32{
   588  							int32(statuspb.TestStatus_PASS_WITH_SKIPS), 3,
   589  						},
   590  					},
   591  				},
   592  			},
   593  			latest:   hours[20],
   594  			earliest: hours[10],
   595  			expected: []inflatedColumn{
   596  				{
   597  					Column: &statepb.Column{
   598  						Build:   "keep-old1",
   599  						Hint:    "keep-old1",
   600  						Started: millis(hours[10]) - 1,
   601  					},
   602  					Cells: map[string]cell{
   603  						"hello": {Result: statuspb.TestStatus_FAIL},
   604  						"world": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
   605  					},
   606  				},
   607  			},
   608  		},
   609  	}
   610  
   611  	for _, tc := range cases {
   612  		t.Run(tc.name, func(t *testing.T) {
   613  			if tc.wantIssues == nil {
   614  				tc.wantIssues = map[string][]string{}
   615  			}
   616  			if tc.ctx == nil {
   617  				tc.ctx = context.Background()
   618  			}
   619  			actual, issues, err := InflateGrid(tc.ctx, tc.grid, tc.earliest, tc.latest)
   620  			switch {
   621  			case err != nil:
   622  				if !tc.err {
   623  					t.Errorf("InflatedGrid() got unexpected error: %v", err)
   624  				}
   625  			case tc.err:
   626  				t.Error("InflateGrid() failed to return an error")
   627  			default:
   628  				if diff := cmp.Diff(tc.expected, actual, cmp.AllowUnexported(inflatedColumn{}, cell{}), protocmp.Transform()); diff != "" {
   629  					t.Errorf("InflateGrid() got unexpected diff (-want +got):\n%s", diff)
   630  				}
   631  				if diff := cmp.Diff(tc.wantIssues, issues); diff != "" {
   632  					t.Errorf("InflateGrid() got unexpected issue diff (-want +got):\n%s", diff)
   633  				}
   634  			}
   635  		})
   636  
   637  	}
   638  }
   639  
   640  func TestInflateRow(t *testing.T) {
   641  	cases := []struct {
   642  		name     string
   643  		row      *statepb.Row
   644  		expected []cell
   645  	}{
   646  		{
   647  			name: "basically works",
   648  		},
   649  		{
   650  			name: "preserve cell ids",
   651  			row: &statepb.Row{
   652  				CellIds:  []string{"cell-a", "cell-b", "cell-d"},
   653  				Icons:    blank(3),
   654  				Messages: blank(3),
   655  				Results: []int32{
   656  					int32(statuspb.TestStatus_PASS), 2,
   657  					int32(statuspb.TestStatus_NO_RESULT), 1,
   658  					int32(statuspb.TestStatus_PASS), 1,
   659  					int32(statuspb.TestStatus_NO_RESULT), 1,
   660  				},
   661  			},
   662  			expected: []cell{
   663  				{
   664  					Result: statuspb.TestStatus_PASS,
   665  					CellID: "cell-a",
   666  				},
   667  				{
   668  					Result: statuspb.TestStatus_PASS,
   669  					CellID: "cell-b",
   670  				},
   671  				{
   672  					Result: statuspb.TestStatus_NO_RESULT,
   673  				},
   674  				{
   675  					Result: statuspb.TestStatus_PASS,
   676  					CellID: "cell-d",
   677  				},
   678  				{
   679  					Result: statuspb.TestStatus_NO_RESULT,
   680  				},
   681  			},
   682  		},
   683  		{
   684  			name: "only finished columns contain icons and messages",
   685  			row: &statepb.Row{
   686  				CellIds: blank(8),
   687  				Icons: []string{
   688  					"F1", "~1", "~2",
   689  				},
   690  				Messages: []string{
   691  					"fail", "flake-first", "flake-second",
   692  				},
   693  				Results: []int32{
   694  					int32(statuspb.TestStatus_NO_RESULT), 2,
   695  					int32(statuspb.TestStatus_FAIL), 1,
   696  					int32(statuspb.TestStatus_NO_RESULT), 2,
   697  					int32(statuspb.TestStatus_FLAKY), 2,
   698  					int32(statuspb.TestStatus_NO_RESULT), 1,
   699  				},
   700  			},
   701  			expected: []cell{
   702  				{},
   703  				{},
   704  				{
   705  					Result:  statuspb.TestStatus_FAIL,
   706  					Icon:    "F1",
   707  					Message: "fail",
   708  				},
   709  				{},
   710  				{},
   711  				{
   712  					Result:  statuspb.TestStatus_FLAKY,
   713  					Icon:    "~1",
   714  					Message: "flake-first",
   715  				},
   716  				{
   717  					Result:  statuspb.TestStatus_FLAKY,
   718  					Icon:    "~2",
   719  					Message: "flake-second",
   720  				},
   721  				{},
   722  			},
   723  		},
   724  		{
   725  			name: "find metric name from row when missing",
   726  			row: &statepb.Row{
   727  				CellIds:  blank(1),
   728  				Icons:    blank(1),
   729  				Messages: blank(1),
   730  				Results: []int32{
   731  					int32(statuspb.TestStatus_PASS), 1,
   732  				},
   733  				Metric: []string{"found-it"},
   734  				Metrics: []*statepb.Metric{
   735  					{
   736  						Indices: []int32{0, 1},
   737  						Values:  []float64{7},
   738  					},
   739  				},
   740  			},
   741  			expected: []cell{
   742  				{
   743  					Result: statuspb.TestStatus_PASS,
   744  					Metrics: map[string]float64{
   745  						"found-it": 7,
   746  					},
   747  				},
   748  			},
   749  		},
   750  		{
   751  			name: "prioritize local metric name",
   752  			row: &statepb.Row{
   753  				CellIds:  blank(1),
   754  				Icons:    blank(1),
   755  				Messages: blank(1),
   756  				Results: []int32{
   757  					int32(statuspb.TestStatus_PASS), 1,
   758  				},
   759  				Metric: []string{"ignore-this"},
   760  				Metrics: []*statepb.Metric{
   761  					{
   762  						Name:    "oh yeah",
   763  						Indices: []int32{0, 1},
   764  						Values:  []float64{7},
   765  					},
   766  				},
   767  			},
   768  			expected: []cell{
   769  				{
   770  					Result: statuspb.TestStatus_PASS,
   771  					Metrics: map[string]float64{
   772  						"oh yeah": 7,
   773  					},
   774  				},
   775  			},
   776  		},
   777  	}
   778  
   779  	for _, tc := range cases {
   780  		t.Run(tc.name, func(t *testing.T) {
   781  			var actual []cell
   782  			nextCell := inflateRow(tc.row)
   783  			for c := nextCell(); c != nil; c = nextCell() {
   784  				actual = append(actual, *c)
   785  			}
   786  
   787  			if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(cell{}), protocmp.Transform()); diff != "" {
   788  				t.Errorf("inflateRow() got unexpected diff (-have, +want):\n%s", diff)
   789  			}
   790  		})
   791  	}
   792  }
   793  
   794  func TestInflateMetric(t *testing.T) {
   795  	point := func(v float64) *float64 {
   796  		return &v
   797  	}
   798  	cases := []struct {
   799  		name     string
   800  		indices  []int32
   801  		values   []float64
   802  		expected []*float64
   803  	}{
   804  		{
   805  			name: "basically works",
   806  		},
   807  		{
   808  			name:    "documented example with both values and holes works",
   809  			indices: []int32{0, 2, 6, 4},
   810  			values:  []float64{0.1, 0.2, 6.1, 6.2, 6.3, 6.4},
   811  			expected: []*float64{
   812  				point(0.1),
   813  				point(0.2),
   814  				nil,
   815  				nil,
   816  				nil,
   817  				nil,
   818  				point(6.1),
   819  				point(6.2),
   820  				point(6.3),
   821  				point(6.4),
   822  			},
   823  		},
   824  	}
   825  
   826  	for _, tc := range cases {
   827  		t.Run(tc.name, func(t *testing.T) {
   828  			var actual []*float64
   829  			metric := statepb.Metric{
   830  				Name:    tc.name,
   831  				Indices: tc.indices,
   832  				Values:  tc.values,
   833  			}
   834  			nextMetric := inflateMetric(&metric)
   835  			for val, ok := nextMetric(); ok; val, ok = nextMetric() {
   836  				actual = append(actual, val)
   837  			}
   838  
   839  			if diff := cmp.Diff(tc.expected, actual); diff != "" {
   840  				t.Errorf("inflateMetric(%v) got unexpected diff (-want +got):\n%s", metric, diff)
   841  			}
   842  		})
   843  	}
   844  }
   845  
   846  func TestInflateResults(t *testing.T) {
   847  	cases := []struct {
   848  		name     string
   849  		results  []int32
   850  		expected []statuspb.TestStatus
   851  	}{
   852  		{
   853  			name: "basically works",
   854  		},
   855  		{
   856  			name: "first documented example with multiple values works",
   857  			results: []int32{
   858  				int32(statuspb.TestStatus_NO_RESULT), 3,
   859  				int32(statuspb.TestStatus_PASS), 4,
   860  			},
   861  			expected: []statuspb.TestStatus{
   862  				statuspb.TestStatus_NO_RESULT,
   863  				statuspb.TestStatus_NO_RESULT,
   864  				statuspb.TestStatus_NO_RESULT,
   865  				statuspb.TestStatus_PASS,
   866  				statuspb.TestStatus_PASS,
   867  				statuspb.TestStatus_PASS,
   868  				statuspb.TestStatus_PASS,
   869  			},
   870  		},
   871  		{
   872  			name: "first item is the type",
   873  			results: []int32{
   874  				int32(statuspb.TestStatus_RUNNING), 1, // RUNNING == 4
   875  			},
   876  			expected: []statuspb.TestStatus{
   877  				statuspb.TestStatus_RUNNING,
   878  			},
   879  		},
   880  		{
   881  			name: "second item is the number of repetitions",
   882  			results: []int32{
   883  				int32(statuspb.TestStatus_PASS), 4, // Running == 1
   884  			},
   885  			expected: []statuspb.TestStatus{
   886  				statuspb.TestStatus_PASS,
   887  				statuspb.TestStatus_PASS,
   888  				statuspb.TestStatus_PASS,
   889  				statuspb.TestStatus_PASS,
   890  			},
   891  		},
   892  	}
   893  
   894  	for _, tc := range cases {
   895  		t.Run(tc.name, func(t *testing.T) {
   896  			nextResult := inflateResults(tc.results)
   897  			var actual []statuspb.TestStatus
   898  			for cur := nextResult(); cur != nil; cur = nextResult() {
   899  				actual = append(actual, *cur)
   900  			}
   901  			if !reflect.DeepEqual(actual, tc.expected) {
   902  				t.Errorf("inflateResults(%v) got %v, want %v", tc.results, actual, tc.expected)
   903  			}
   904  		})
   905  	}
   906  }