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

     1  /*
     2  Copyright 2018 The Kubernetes 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  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"net/http"
    25  	"reflect"
    26  	"sort"
    27  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	"cloud.google.com/go/storage"
    32  	"github.com/GoogleCloudPlatform/testgrid/config"
    33  	"github.com/GoogleCloudPlatform/testgrid/metadata"
    34  	_ "github.com/GoogleCloudPlatform/testgrid/metadata/junit"
    35  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    36  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    37  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    38  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    39  	"github.com/GoogleCloudPlatform/testgrid/util/gcs/fake"
    40  	"github.com/fvbommel/sortorder"
    41  	"github.com/golang/protobuf/ptypes/timestamp"
    42  	"github.com/google/go-cmp/cmp"
    43  	"github.com/sirupsen/logrus"
    44  	"google.golang.org/api/googleapi"
    45  	"google.golang.org/protobuf/testing/protocmp"
    46  	core "k8s.io/api/core/v1"
    47  )
    48  
    49  type fakeUpload = fake.Upload
    50  type fakeStater = fake.Stater
    51  type fakeStat = fake.Stat
    52  type fakeUploader = fake.Uploader
    53  type fakeUploadClient = fake.UploadClient
    54  type fakeLister = fake.Lister
    55  type fakeOpener = fake.Opener
    56  
    57  func TestGCS(t *testing.T) {
    58  	cases := []struct {
    59  		name  string
    60  		ctx   context.Context
    61  		group *configpb.TestGroup
    62  		fail  bool
    63  	}{
    64  		{
    65  			name:  "contextless",
    66  			group: &configpb.TestGroup{},
    67  			fail:  true,
    68  		},
    69  		{
    70  			name:  "basic",
    71  			ctx:   context.Background(),
    72  			group: &configpb.TestGroup{},
    73  		},
    74  		{
    75  			name: "kubernetes", // should fail
    76  			ctx:  context.Background(),
    77  			group: &configpb.TestGroup{
    78  				UseKubernetesClient: true,
    79  			},
    80  			fail: true,
    81  		},
    82  	}
    83  
    84  	for _, tc := range cases {
    85  		t.Run(tc.name, func(t *testing.T) {
    86  			// Goal here is to ignore for non-k8s client otherwise if we get past this check
    87  			// send updater() arguments that should fail if it tries to do anything,
    88  			// either because the context is canceled or things like client are unset)
    89  			ctx, cancel := context.WithCancel(context.Background())
    90  			cancel()
    91  			defer func() {
    92  				if r := recover(); r != nil {
    93  					if !tc.fail {
    94  						t.Errorf("updater() got an unexpected panic: %#v", r)
    95  					}
    96  				}
    97  			}()
    98  			updater := GCS(tc.ctx, nil, 0, 0, 0, false, false)
    99  			_, err := updater(ctx, logrus.WithField("case", tc.name), nil, tc.group, gcs.Path{})
   100  			switch {
   101  			case err != nil:
   102  				if !tc.fail {
   103  					t.Errorf("updater() got unexpected error: %v", err)
   104  				}
   105  			case tc.fail:
   106  				t.Error("updater() failed to return an error")
   107  			}
   108  		})
   109  	}
   110  }
   111  
   112  func TestUpdate(t *testing.T) {
   113  	defaultTimeout := 5 * time.Minute
   114  	configPath := newPathOrDie("gs://bucket/path/to/config")
   115  	cases := []struct {
   116  		name             string
   117  		ctx              context.Context
   118  		config           *configpb.Configuration
   119  		configErr        error
   120  		builds           map[string][]fakeBuild
   121  		gridPrefix       string
   122  		groupConcurrency int
   123  		buildConcurrency int
   124  		skipConfirm      bool
   125  		groupUpdater     GroupUpdater
   126  		groupTimeout     *time.Duration
   127  		buildTimeout     *time.Duration
   128  		groupNames       []string
   129  		freq             time.Duration
   130  
   131  		expected  fakeUploader
   132  		err       bool
   133  		successes int
   134  		errors    int
   135  		skips     int
   136  	}{
   137  		{
   138  			name: "basically works",
   139  			config: &configpb.Configuration{
   140  				TestGroups: []*configpb.TestGroup{
   141  					{
   142  						Name:                "hello",
   143  						GcsPrefix:           "kubernetes-jenkins/path/to/job",
   144  						DaysOfResults:       7,
   145  						UseKubernetesClient: true,
   146  						NumColumnsRecent:    6,
   147  					},
   148  					{
   149  						Name:             "modern",
   150  						DaysOfResults:    7,
   151  						NumColumnsRecent: 6,
   152  						ResultSource: &configpb.TestGroup_ResultSource{
   153  							ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{
   154  								GcsConfig: &configpb.GCSConfig{
   155  									GcsPrefix: "kubernetes-jenkins/path/to/another-job",
   156  								},
   157  							},
   158  						},
   159  					},
   160  					{
   161  						Name:             "skip-non-k8s",
   162  						GcsPrefix:        "kubernetes-jenkins/path/to/job",
   163  						DaysOfResults:    7,
   164  						NumColumnsRecent: 6,
   165  					},
   166  				},
   167  				Dashboards: []*configpb.Dashboard{
   168  					{
   169  						Name: "dash",
   170  						DashboardTab: []*configpb.DashboardTab{
   171  							{
   172  								Name:          "hello-tab",
   173  								TestGroupName: "hello",
   174  							},
   175  							{
   176  								Name:          "modern-tab",
   177  								TestGroupName: "modern",
   178  							},
   179  							{
   180  								Name:          "skip-tab",
   181  								TestGroupName: "skip-non-k8s",
   182  							},
   183  						},
   184  					},
   185  				},
   186  			},
   187  			expected: fakeUploader{
   188  				*resolveOrDie(&configPath, "hello"): {
   189  					Buf:          mustGrid(&statepb.Grid{}),
   190  					CacheControl: "no-cache",
   191  					WorldRead:    gcs.DefaultACL,
   192  					Generation:   2,
   193  				},
   194  				*resolveOrDie(&configPath, "modern"): {
   195  					Buf:          mustGrid(&statepb.Grid{}),
   196  					CacheControl: "no-cache",
   197  					Generation:   2,
   198  					WorldRead:    gcs.DefaultACL,
   199  				},
   200  				*resolveOrDie(&configPath, "skip-non-k8s"): {
   201  					Buf:          mustGrid(&statepb.Grid{}),
   202  					CacheControl: "no-cache",
   203  					WorldRead:    gcs.DefaultACL,
   204  					Generation:   1,
   205  				},
   206  			},
   207  			successes: 3,
   208  		},
   209  		{
   210  			name:       "bad grid prefix",
   211  			gridPrefix: "!@#$%^&*()",
   212  			config: &configpb.Configuration{
   213  				TestGroups: []*configpb.TestGroup{
   214  					{
   215  						Name:                "hello",
   216  						GcsPrefix:           "kubernetes-jenkins/path/to/job",
   217  						DaysOfResults:       7,
   218  						UseKubernetesClient: true,
   219  						NumColumnsRecent:    6,
   220  					},
   221  				},
   222  				Dashboards: []*configpb.Dashboard{
   223  					{
   224  						Name: "dash",
   225  						DashboardTab: []*configpb.DashboardTab{
   226  							{
   227  								Name:          "hello-tab",
   228  								TestGroupName: "hello",
   229  							},
   230  						},
   231  					},
   232  				},
   233  			},
   234  			expected: fakeUploader{},
   235  			err:      true,
   236  		},
   237  		{
   238  			name: "update specified",
   239  			config: &configpb.Configuration{
   240  				TestGroups: []*configpb.TestGroup{
   241  					{
   242  						Name:                "hello",
   243  						GcsPrefix:           "kubernetes-jenkins/path/to/job",
   244  						DaysOfResults:       7,
   245  						UseKubernetesClient: true,
   246  						NumColumnsRecent:    6,
   247  					},
   248  					{
   249  						Name:                "hiya",
   250  						GcsPrefix:           "kubernetes-jenkins/path/to/job",
   251  						DaysOfResults:       7,
   252  						UseKubernetesClient: true,
   253  						NumColumnsRecent:    6,
   254  					},
   255  					{
   256  						Name:                "goodbye",
   257  						GcsPrefix:           "kubernetes-jenkins/path/to/job",
   258  						DaysOfResults:       7,
   259  						UseKubernetesClient: true,
   260  						NumColumnsRecent:    6,
   261  					},
   262  				},
   263  				Dashboards: []*configpb.Dashboard{
   264  					{
   265  						Name: "dash",
   266  						DashboardTab: []*configpb.DashboardTab{
   267  							{
   268  								Name:          "hello-tab",
   269  								TestGroupName: "hello",
   270  							},
   271  							{
   272  								Name:          "hiya-tab",
   273  								TestGroupName: "hiya",
   274  							},
   275  							{
   276  								Name:          "goodbye-tab",
   277  								TestGroupName: "goodbye",
   278  							},
   279  						},
   280  					},
   281  				},
   282  			},
   283  			groupNames: []string{"hello", "hiya"},
   284  			expected: fakeUploader{
   285  				*resolveOrDie(&configPath, "hello"): {
   286  					Buf:          mustGrid(&statepb.Grid{}),
   287  					CacheControl: "no-cache",
   288  					WorldRead:    gcs.DefaultACL,
   289  					Generation:   2,
   290  				},
   291  				*resolveOrDie(&configPath, "hiya"): {
   292  					Buf:          mustGrid(&statepb.Grid{}),
   293  					CacheControl: "no-cache",
   294  					Generation:   2,
   295  					WorldRead:    gcs.DefaultACL,
   296  				},
   297  			},
   298  			successes: 2,
   299  		},
   300  		{
   301  			name: "update error with freq = 0",
   302  			config: &configpb.Configuration{
   303  				TestGroups: []*configpb.TestGroup{
   304  					{
   305  						Name:                "hello",
   306  						GcsPrefix:           "kubernetes-jenkins/path/to/job",
   307  						DaysOfResults:       7,
   308  						UseKubernetesClient: true,
   309  						NumColumnsRecent:    6,
   310  					},
   311  					{
   312  						Name:                "world",
   313  						GcsPrefix:           "kubernetes-jenkins/path/to/job",
   314  						DaysOfResults:       7,
   315  						UseKubernetesClient: true,
   316  						NumColumnsRecent:    6,
   317  					},
   318  				},
   319  				Dashboards: []*configpb.Dashboard{
   320  					{
   321  						Name: "dash",
   322  						DashboardTab: []*configpb.DashboardTab{
   323  							{
   324  								Name:          "hello-tab",
   325  								TestGroupName: "hello",
   326  							},
   327  							{
   328  								Name:          "world-tab",
   329  								TestGroupName: "world",
   330  							},
   331  						},
   332  					},
   333  				},
   334  			},
   335  			groupUpdater: func(_ context.Context, _ logrus.FieldLogger, _ gcs.Client, tg *configpb.TestGroup, _ gcs.Path) (bool, error) {
   336  				if tg.Name == "world" {
   337  					return false, &googleapi.Error{
   338  						Code: http.StatusPreconditionFailed,
   339  					}
   340  				}
   341  				return false, errors.New("bad update")
   342  
   343  			},
   344  			builds: make(map[string][]fakeBuild),
   345  			freq:   time.Duration(0),
   346  			expected: fakeUploader{
   347  				*resolveOrDie(&configPath, "hello"): {
   348  					Buf:          mustGrid(&statepb.Grid{}),
   349  					CacheControl: "no-cache",
   350  					WorldRead:    gcs.DefaultACL,
   351  					Generation:   1,
   352  				},
   353  				*resolveOrDie(&configPath, "world"): {
   354  					Buf:          mustGrid(&statepb.Grid{}),
   355  					CacheControl: "no-cache",
   356  					WorldRead:    gcs.DefaultACL,
   357  					Generation:   1,
   358  				},
   359  			},
   360  			errors: 1,
   361  			skips:  1,
   362  		},
   363  		// TODO(fejta): more cases
   364  	}
   365  
   366  	for _, tc := range cases {
   367  		t.Run(tc.name, func(t *testing.T) {
   368  			if tc.ctx == nil {
   369  				tc.ctx = context.Background()
   370  			}
   371  			ctx, cancel := context.WithCancel(tc.ctx)
   372  			defer cancel()
   373  
   374  			if tc.groupConcurrency == 0 {
   375  				tc.groupConcurrency = 1
   376  			}
   377  			if tc.buildConcurrency == 0 {
   378  				tc.buildConcurrency = 1
   379  			}
   380  			if tc.groupTimeout == nil {
   381  				tc.groupTimeout = &defaultTimeout
   382  			}
   383  			if tc.buildTimeout == nil {
   384  				tc.buildTimeout = &defaultTimeout
   385  			}
   386  
   387  			client := &fake.ConditionalClient{
   388  				UploadClient: fake.UploadClient{
   389  					Uploader: fakeUploader{},
   390  					Client: fakeClient{
   391  						Lister: fakeLister{},
   392  						Opener: fakeOpener{
   393  							Paths: map[gcs.Path]fake.Object{},
   394  						},
   395  					},
   396  				},
   397  				Lock: &sync.RWMutex{},
   398  			}
   399  
   400  			client.Opener.Paths[configPath] = fakeObject{
   401  				Data: func() string {
   402  					b, err := config.MarshalBytes(tc.config)
   403  					if err != nil {
   404  						t.Fatalf("config.MarshalBytes() errored: %v", err)
   405  					}
   406  					return string(b)
   407  				}(),
   408  				Attrs:   &storage.ReaderObjectAttrs{},
   409  				ReadErr: tc.configErr,
   410  			}
   411  
   412  			for _, group := range tc.config.TestGroups {
   413  				builds, ok := tc.builds[group.Name]
   414  				if !ok {
   415  					continue
   416  				}
   417  				buildsPath := newPathOrDie("gs://" + group.GcsPrefix)
   418  				fi := client.Lister[buildsPath]
   419  				for _, build := range addBuilds(&client.Client, buildsPath, builds...) {
   420  					fi.Objects = append(fi.Objects, storage.ObjectAttrs{
   421  						Prefix: build.Path.Object(),
   422  					})
   423  				}
   424  				client.Lister[buildsPath] = fi
   425  			}
   426  
   427  			if tc.groupUpdater == nil {
   428  				poolCtx, poolCancel := context.WithCancel(context.Background())
   429  				defer poolCancel()
   430  				tc.groupUpdater = GCS(poolCtx, client, *tc.groupTimeout, *tc.buildTimeout, tc.buildConcurrency, !tc.skipConfirm, false)
   431  			}
   432  			opts := &UpdateOptions{
   433  				ConfigPath:       configPath,
   434  				GridPrefix:       tc.gridPrefix,
   435  				GroupConcurrency: tc.groupConcurrency,
   436  				GroupNames:       tc.groupNames,
   437  				Write:            !tc.skipConfirm,
   438  				Freq:             tc.freq,
   439  			}
   440  			err := Update(
   441  				ctx,
   442  				client,
   443  				nil, // metric,
   444  				tc.groupUpdater,
   445  				opts,
   446  			)
   447  			switch {
   448  			case err != nil:
   449  				if !tc.err {
   450  					t.Errorf("Update() got unexpected error: %v", err)
   451  				}
   452  			case tc.err:
   453  				t.Error("Update() failed to receive an error")
   454  			default:
   455  				actual := client.Uploader
   456  				if diff := cmp.Diff(tc.expected, actual, cmp.AllowUnexported(fakeUpload{})); diff != "" {
   457  					t.Errorf("Update() uploaded files got unexpected diff (-want, +got):\n%s", diff)
   458  				}
   459  			}
   460  		})
   461  	}
   462  }
   463  
   464  func TestTestGroupPath(t *testing.T) {
   465  	path := newPathOrDie("gs://bucket/config")
   466  	pNewPathOrDie := func(s string) *gcs.Path {
   467  		p := newPathOrDie(s)
   468  		return &p
   469  	}
   470  	cases := []struct {
   471  		name       string
   472  		groupName  string
   473  		gridPrefix string
   474  		expected   *gcs.Path
   475  	}{
   476  		{
   477  			name:     "basically works",
   478  			expected: &path,
   479  		},
   480  		{
   481  			name:      "invalid group name errors",
   482  			groupName: "---://foo",
   483  		},
   484  		{
   485  			name:      "bucket change errors",
   486  			groupName: "gs://honey-bucket/config",
   487  		},
   488  		{
   489  			name:      "normal behavior works",
   490  			groupName: "random-group",
   491  			expected:  pNewPathOrDie("gs://bucket/random-group"),
   492  		},
   493  		{
   494  			name:      "target a subfolder works",
   495  			groupName: "beta/random-group",
   496  			expected:  pNewPathOrDie("gs://bucket/beta/random-group"),
   497  		},
   498  		{
   499  			name:      "resolve reference fails",
   500  			groupName: "http://bucket/config",
   501  		},
   502  	}
   503  
   504  	for _, tc := range cases {
   505  		t.Run(tc.name, func(t *testing.T) {
   506  			actual, err := TestGroupPath(path, tc.gridPrefix, tc.groupName)
   507  			switch {
   508  			case err != nil:
   509  				if tc.expected != nil {
   510  					t.Errorf("testGroupPath(%v, %v) got unexpected error: %v", path, tc.groupName, err)
   511  				}
   512  			case tc.expected == nil:
   513  				t.Errorf("testGroupPath(%v, %v) failed to receive an error", path, tc.groupName)
   514  			default:
   515  				if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcs.Path{})); diff != "" {
   516  					t.Errorf("testGroupPath(%v, %v) got unexpected diff (-have, +want):\n%s", path, tc.groupName, diff)
   517  				}
   518  			}
   519  		})
   520  	}
   521  }
   522  
   523  func jsonStarted(stamp int64) *fakeObject {
   524  	return &fakeObject{
   525  		Data: jsonData(metadata.Started{Timestamp: stamp}),
   526  	}
   527  }
   528  
   529  func jsonFinished(stamp int64, passed bool, meta metadata.Metadata) *fakeObject {
   530  	return &fakeObject{
   531  		Data: jsonData(metadata.Finished{
   532  			Timestamp: &stamp,
   533  			Passed:    &passed,
   534  			Metadata:  meta,
   535  		}),
   536  	}
   537  }
   538  
   539  var (
   540  	podInfoSuccessPodInfo = gcs.PodInfo{
   541  		Pod: &core.Pod{
   542  			Status: core.PodStatus{
   543  				Phase: core.PodSucceeded,
   544  			},
   545  		},
   546  	}
   547  	podInfoSuccess     = jsonPodInfo(podInfoSuccessPodInfo)
   548  	podInfoPassCell    = cell{Result: statuspb.TestStatus_PASS}
   549  	podInfoMissingCell = cell{
   550  		Result:  statuspb.TestStatus_RUNNING,
   551  		Icon:    "!",
   552  		Message: gcs.MissingPodInfo,
   553  	}
   554  )
   555  
   556  func jsonPodInfo(podInfo gcs.PodInfo) *fakeObject {
   557  	return &fakeObject{Data: jsonData(podInfo)}
   558  }
   559  
   560  func mustGrid(grid *statepb.Grid) []byte {
   561  	buf, err := gcs.MarshalGrid(grid)
   562  	if err != nil {
   563  		panic(err)
   564  	}
   565  	return buf
   566  }
   567  
   568  func TestTruncateRunning(t *testing.T) {
   569  	now := float64(time.Now().UTC().Unix() * 1000)
   570  	floor := time.Now().Add(-72 * time.Hour)
   571  	ancient := float64(time.Now().Add(-74*time.Hour).UTC().Unix() * 1000)
   572  	cases := []struct {
   573  		name     string
   574  		cols     []inflatedColumn
   575  		expected func([]inflatedColumn) []inflatedColumn
   576  	}{
   577  		{
   578  			name: "basically works",
   579  		},
   580  		{
   581  			name: "keep everything (no Overall)",
   582  			cols: []inflatedColumn{
   583  				{
   584  					Column: &statepb.Column{
   585  						Build:   "this",
   586  						Started: now,
   587  					},
   588  				},
   589  				{
   590  					Column: &statepb.Column{
   591  						Build:   "that",
   592  						Started: now,
   593  					},
   594  				},
   595  				{
   596  					Column: &statepb.Column{
   597  						Build:   "another",
   598  						Started: now,
   599  					},
   600  				},
   601  			},
   602  		},
   603  		{
   604  			name: "keep everything completed",
   605  			cols: []inflatedColumn{
   606  				{
   607  					Column: &statepb.Column{
   608  						Build:   "passed",
   609  						Started: now,
   610  					},
   611  					Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_PASS}},
   612  				},
   613  				{
   614  					Column: &statepb.Column{
   615  						Build:   "failed",
   616  						Started: now,
   617  					},
   618  					Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_FAIL}},
   619  				},
   620  			},
   621  		},
   622  		{
   623  			name: "drop everything before oldest running",
   624  			cols: []inflatedColumn{
   625  				{
   626  					Column: &statepb.Column{
   627  						Build:   "this1",
   628  						Started: now,
   629  					},
   630  				},
   631  				{
   632  					Column: &statepb.Column{
   633  						Build:   "this2",
   634  						Started: now,
   635  					},
   636  				},
   637  				{
   638  					Column: &statepb.Column{
   639  						Build:   "running1",
   640  						Started: now,
   641  					},
   642  					Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}},
   643  				},
   644  				{
   645  					Column: &statepb.Column{
   646  						Build:   "this3",
   647  						Started: now,
   648  					},
   649  				},
   650  				{
   651  					Column: &statepb.Column{
   652  						Build:   "running2",
   653  						Started: now,
   654  					},
   655  					Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}},
   656  				},
   657  				{
   658  					Column: &statepb.Column{
   659  						Build:   "this4",
   660  						Started: now,
   661  					},
   662  				},
   663  				{
   664  					Column: &statepb.Column{
   665  						Build:   "this5",
   666  						Started: now,
   667  					},
   668  				},
   669  				{
   670  					Column: &statepb.Column{
   671  						Build:   "this6",
   672  						Started: now,
   673  					},
   674  				},
   675  				{
   676  					Column: &statepb.Column{
   677  						Build:   "this7",
   678  						Started: now,
   679  					},
   680  				},
   681  			},
   682  			expected: func(cols []inflatedColumn) []inflatedColumn {
   683  				return cols[5:] // this4 and earlier
   684  			},
   685  		},
   686  		{
   687  			name: "drop all as all are running",
   688  			cols: []inflatedColumn{
   689  				{
   690  					Column: &statepb.Column{
   691  						Build:   "running1",
   692  						Started: now,
   693  					},
   694  					Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}},
   695  				},
   696  				{
   697  					Column: &statepb.Column{
   698  						Build:   "running2",
   699  						Started: now,
   700  					},
   701  					Cells: map[string]cell{overallRow: {Result: statuspb.TestStatus_RUNNING}},
   702  				},
   703  			},
   704  			expected: func(cols []inflatedColumn) []inflatedColumn {
   705  				return cols[2:]
   706  			},
   707  		},
   708  		{
   709  			name: "drop running columns if any process is running",
   710  			cols: []InflatedColumn{
   711  				{
   712  					Column: &statepb.Column{
   713  						Build:   "running",
   714  						Started: now,
   715  					},
   716  					Cells: map[string]cell{
   717  						"process1": {Result: statuspb.TestStatus_RUNNING},
   718  						"process2": {Result: statuspb.TestStatus_RUNNING},
   719  					},
   720  				},
   721  				{
   722  					Column: &statepb.Column{
   723  						Build:   "running-partially",
   724  						Started: now,
   725  					},
   726  					Cells: map[string]cell{
   727  						"process1": {Result: statuspb.TestStatus_RUNNING},
   728  						"process2": {Result: statuspb.TestStatus_PASS},
   729  					},
   730  				},
   731  				{
   732  					Column: &statepb.Column{
   733  						Build:   "ok",
   734  						Started: now,
   735  					},
   736  					Cells: map[string]cell{
   737  						"process1": {Result: statuspb.TestStatus_PASS},
   738  						"process2": {Result: statuspb.TestStatus_PASS},
   739  					},
   740  				},
   741  			},
   742  			expected: func(cols []inflatedColumn) []inflatedColumn {
   743  				return cols[2:]
   744  			},
   745  		},
   746  		{
   747  			name: "ignore ancient running columns",
   748  			cols: []InflatedColumn{
   749  				{
   750  					Column: &statepb.Column{
   751  						Build:   "recent-running",
   752  						Started: now,
   753  					},
   754  					Cells: map[string]cell{"drop": {Result: statuspb.TestStatus_RUNNING}},
   755  				},
   756  				{
   757  					Column: &statepb.Column{
   758  						Build:   "recent-done",
   759  						Started: now - 1,
   760  					},
   761  					Cells: map[string]cell{"keep": {Result: statuspb.TestStatus_PASS}},
   762  				},
   763  
   764  				{
   765  					Column: &statepb.Column{
   766  						Build:   "running-ancient",
   767  						Started: ancient,
   768  					},
   769  					Cells: map[string]cell{"too-old-to-drop": {Result: statuspb.TestStatus_RUNNING}},
   770  				},
   771  				{
   772  					Column: &statepb.Column{
   773  						Build:   "ok",
   774  						Started: ancient - 1,
   775  					},
   776  					Cells: map[string]cell{"also keep": {Result: statuspb.TestStatus_PASS}},
   777  				},
   778  			},
   779  			expected: func(cols []inflatedColumn) []inflatedColumn {
   780  				return cols[1:]
   781  			},
   782  		},
   783  	}
   784  
   785  	for _, tc := range cases {
   786  		t.Run(tc.name, func(t *testing.T) {
   787  			actual := truncateRunning(tc.cols, floor)
   788  			expected := tc.cols
   789  			if tc.expected != nil {
   790  				expected = tc.expected(expected)
   791  			}
   792  			if diff := cmp.Diff(actual, expected, protocmp.Transform()); diff != "" {
   793  				t.Errorf("truncateRunning() got unexpected diff:\n%s", diff)
   794  			}
   795  		})
   796  	}
   797  }
   798  
   799  func TestListBuilds(t *testing.T) {
   800  	cases := []struct {
   801  		name     string
   802  		since    string
   803  		client   fakeLister
   804  		paths    []gcs.Path
   805  		expected []gcs.Build
   806  		err      bool
   807  	}{
   808  		{
   809  			name: "basically works",
   810  		},
   811  		{
   812  			name: "list err",
   813  			client: fakeLister{
   814  				newPathOrDie("gs://prefix/job/"): fakeIterator{
   815  					Objects: []storage.ObjectAttrs{
   816  						{
   817  							Prefix: "job/1/",
   818  						},
   819  						{
   820  							Prefix: "job/10/",
   821  						},
   822  						{
   823  							Prefix: "job/2/",
   824  						},
   825  					},
   826  					Err: 1,
   827  				},
   828  			},
   829  			paths: []gcs.Path{
   830  				newPathOrDie("gs://prefix/job/"),
   831  			},
   832  			err: true,
   833  		},
   834  		{
   835  			name: "bucket err",
   836  			client: fakeLister{
   837  				newPathOrDie("gs://prefix/job/"): fakeIterator{
   838  					Objects: []storage.ObjectAttrs{
   839  						{
   840  							Prefix: "job/1/",
   841  						},
   842  						{
   843  							Prefix: "job/10/",
   844  						},
   845  						{
   846  							Prefix: "job/2/",
   847  						},
   848  					},
   849  					ErrOpen: storage.ErrBucketNotExist,
   850  				},
   851  			},
   852  			paths: []gcs.Path{
   853  				newPathOrDie("gs://prefix/job/"),
   854  			},
   855  			err: true,
   856  		},
   857  		{
   858  			name: "list stuff correctly",
   859  			client: fakeLister{
   860  				newPathOrDie("gs://prefix/job/"): fakeIterator{
   861  					Objects: []storage.ObjectAttrs{
   862  						{
   863  							Prefix: "job/1/",
   864  						},
   865  						{
   866  							Prefix: "job/10/",
   867  						},
   868  						{
   869  							Prefix: "job/2/",
   870  						},
   871  					},
   872  				},
   873  			},
   874  			paths: []gcs.Path{
   875  				newPathOrDie("gs://prefix/job/"),
   876  			},
   877  			expected: []gcs.Build{
   878  				{
   879  					Path: newPathOrDie("gs://prefix/job/10/"),
   880  				},
   881  				{
   882  					Path: newPathOrDie("gs://prefix/job/2/"),
   883  				},
   884  				{
   885  					Path: newPathOrDie("gs://prefix/job/1/"),
   886  				},
   887  			},
   888  		},
   889  		{
   890  			name:  "list offsets correctly",
   891  			since: "3",
   892  			client: fakeLister{
   893  				newPathOrDie("gs://prefix/job/"): fakeIterator{
   894  					Objects: []storage.ObjectAttrs{
   895  						{
   896  							Prefix: "job/1/",
   897  						},
   898  						{
   899  							Prefix: "job/10/",
   900  						},
   901  						{
   902  							Prefix: "job/2/",
   903  						},
   904  						{
   905  							Prefix: "job/3/",
   906  						},
   907  						{
   908  							Prefix: "job/4/",
   909  						},
   910  					},
   911  				},
   912  			},
   913  			paths: []gcs.Path{
   914  				newPathOrDie("gs://prefix/job/"),
   915  			},
   916  			expected: []gcs.Build{
   917  				{
   918  					Path: newPathOrDie("gs://prefix/job/10/"),
   919  				},
   920  				{
   921  					Path: newPathOrDie("gs://prefix/job/4/"),
   922  				},
   923  			},
   924  		},
   925  		{
   926  			name: "collate stuff correctly",
   927  			client: fakeLister{
   928  				newPathOrDie("gs://prefix/job/"): fakeIterator{
   929  					Objects: []storage.ObjectAttrs{
   930  						{
   931  							Prefix: "job/1/",
   932  						},
   933  						{
   934  							Prefix: "job/10/",
   935  						},
   936  						{
   937  							Prefix: "job/3/",
   938  						},
   939  					},
   940  				},
   941  				newPathOrDie("gs://other-prefix/presubmit-job/"): fakeIterator{
   942  					Objects: []storage.ObjectAttrs{
   943  						{
   944  							Name: "job/2",
   945  							Metadata: map[string]string{
   946  								"link": "gs://foo/bar333", // intentionally larger than job 20 and 4
   947  							},
   948  						},
   949  						{
   950  							Name: "job/20",
   951  							Metadata: map[string]string{
   952  								"link": "gs://foo/bar222",
   953  							},
   954  						},
   955  						{
   956  
   957  							Name: "job/4",
   958  							Metadata: map[string]string{
   959  								"link": "gs://foo/bar111",
   960  							},
   961  						},
   962  					},
   963  				},
   964  			},
   965  			paths: []gcs.Path{
   966  				newPathOrDie("gs://prefix/job/"),
   967  				newPathOrDie("gs://other-prefix/presubmit-job/"),
   968  			},
   969  			expected: []gcs.Build{
   970  				{
   971  					Path: newPathOrDie("gs://foo/bar222/"),
   972  					// baseName: 20
   973  				},
   974  				{
   975  					Path: newPathOrDie("gs://prefix/job/10/"),
   976  				},
   977  				{
   978  					Path: newPathOrDie("gs://foo/bar111/"),
   979  					// baseName: 4
   980  				},
   981  				{
   982  					Path: newPathOrDie("gs://prefix/job/3/"),
   983  				},
   984  				{
   985  					Path: newPathOrDie("gs://foo/bar333/"),
   986  					// baseName: 2
   987  				},
   988  				{
   989  					Path: newPathOrDie("gs://prefix/job/1/"),
   990  				},
   991  			},
   992  		},
   993  		{
   994  			name:  "collated offsets work correctly",
   995  			since: "5", // drop 4 3 2 1, keep 20, 10
   996  			client: fakeLister{
   997  				newPathOrDie("gs://prefix/job/"): fakeIterator{
   998  					Objects: []storage.ObjectAttrs{
   999  						{
  1000  							Prefix: "job/1/",
  1001  						},
  1002  						{
  1003  							Prefix: "job/10/",
  1004  						},
  1005  						{
  1006  							Prefix: "job/3/",
  1007  						},
  1008  					},
  1009  				},
  1010  				newPathOrDie("gs://other-prefix/presubmit-job/"): fakeIterator{
  1011  					Objects: []storage.ObjectAttrs{
  1012  						{
  1013  							Name: "job/2",
  1014  							Metadata: map[string]string{
  1015  								"link": "gs://foo/bar333", // intentionally larger than job 20 and 4
  1016  							},
  1017  						},
  1018  						{
  1019  							Name: "job/20",
  1020  							Metadata: map[string]string{
  1021  								"link": "gs://foo/bar222",
  1022  							},
  1023  						},
  1024  						{
  1025  
  1026  							Name: "job/4",
  1027  							Metadata: map[string]string{
  1028  								"link": "gs://foo/bar111",
  1029  							},
  1030  						},
  1031  					},
  1032  				},
  1033  			},
  1034  			paths: []gcs.Path{
  1035  				newPathOrDie("gs://prefix/job/"),
  1036  				newPathOrDie("gs://other-prefix/presubmit-job/"),
  1037  			},
  1038  			expected: []gcs.Build{
  1039  				{
  1040  					Path: newPathOrDie("gs://foo/bar222/"),
  1041  					// baseName: 20
  1042  				},
  1043  				{
  1044  					Path: newPathOrDie("gs://prefix/job/10/"),
  1045  				},
  1046  			},
  1047  		},
  1048  	}
  1049  
  1050  	compareBuilds := cmp.Comparer(func(x, y gcs.Build) bool {
  1051  		return x.String() == y.String()
  1052  	})
  1053  	ctx := context.Background()
  1054  	for _, tc := range cases {
  1055  		t.Run(tc.name, func(t *testing.T) {
  1056  			actual, err := listBuilds(ctx, tc.client, tc.since, tc.paths...)
  1057  			switch {
  1058  			case err != nil:
  1059  				if !tc.err {
  1060  					t.Errorf("listBuilds() got unexpected error: %v", err)
  1061  				}
  1062  			case tc.err:
  1063  				t.Errorf("listBuilds() failed to return an error")
  1064  			default:
  1065  				if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcs.Path{}), compareBuilds); diff != "" {
  1066  					t.Errorf("listBuilds() got unexpected diff (-have, +want):\n%s", diff)
  1067  				}
  1068  			}
  1069  		})
  1070  	}
  1071  }
  1072  
  1073  func TestInflateDropAppend(t *testing.T) {
  1074  	const dayAgo = 60 * 60 * 24
  1075  	now := time.Now().Unix()
  1076  	uploadPath := newPathOrDie("gs://fake/upload/location")
  1077  	defaultTimeout := 5 * time.Minute
  1078  	// a simple ColumnReader that parses fakeBuilds
  1079  	fakeColReader := func(builds []fakeBuild) ColumnReader {
  1080  		return func(ctx context.Context, _ logrus.FieldLogger, _ *configpb.TestGroup, _ []InflatedColumn, _ time.Time, receivers chan<- InflatedColumn) error {
  1081  			ctx, cancel := context.WithCancel(ctx)
  1082  			defer cancel() // do not leak go routines
  1083  			for i := len(builds) - 1; i >= 0; i-- {
  1084  				b := builds[i]
  1085  				started := metadata.Started{}
  1086  				if err := json.Unmarshal([]byte(b.started.Data), &started); err != nil {
  1087  					return err
  1088  				}
  1089  				col := InflatedColumn{
  1090  					Column: &statepb.Column{
  1091  						Build:   b.id,
  1092  						Started: float64(started.Timestamp * 1000),
  1093  						Hint:    b.id,
  1094  					},
  1095  					Cells: map[string]Cell{},
  1096  				}
  1097  				for _, cell := range b.passed {
  1098  					col.Cells[cell] = Cell{Result: statuspb.TestStatus_PASS}
  1099  				}
  1100  				for _, cell := range b.failed {
  1101  					col.Cells[cell] = Cell{Result: statuspb.TestStatus_FAIL}
  1102  				}
  1103  				select {
  1104  				case <-ctx.Done():
  1105  					return ctx.Err()
  1106  				case receivers <- col:
  1107  				}
  1108  			}
  1109  			return nil
  1110  		}
  1111  	}
  1112  	cases := []struct {
  1113  		name         string
  1114  		ctx          context.Context
  1115  		builds       []fakeBuild
  1116  		group        *configpb.TestGroup
  1117  		concurrency  int
  1118  		skipWrite    bool
  1119  		colReader    func(builds []fakeBuild) ColumnReader
  1120  		reprocess    time.Duration
  1121  		groupTimeout *time.Duration
  1122  		buildTimeout *time.Duration
  1123  		current      *fake.Object
  1124  		expected     *fakeUpload
  1125  		err          bool
  1126  	}{
  1127  		{
  1128  			name: "basically works",
  1129  			group: &configpb.TestGroup{
  1130  				GcsPrefix: "bucket/path/to/build/",
  1131  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  1132  					{
  1133  						ConfigurationValue: "Commit",
  1134  					},
  1135  				},
  1136  			},
  1137  			builds: []fakeBuild{
  1138  				{
  1139  					id:      "99",
  1140  					started: jsonStarted(now + 99),
  1141  				},
  1142  				{
  1143  					id:      "80",
  1144  					started: jsonStarted(now + 80),
  1145  					podInfo: podInfoSuccess,
  1146  					finished: jsonFinished(now+81, true, metadata.Metadata{
  1147  						metadata.JobVersion: "build80",
  1148  					}),
  1149  					passed: []string{"good1", "good2", "flaky"},
  1150  				},
  1151  				{
  1152  					id:      "50",
  1153  					started: jsonStarted(now + 50),
  1154  					podInfo: podInfoSuccess,
  1155  					finished: jsonFinished(now+51, false, metadata.Metadata{
  1156  						metadata.JobVersion: "build50",
  1157  					}),
  1158  					passed: []string{"good1", "good2"},
  1159  					failed: []string{"flaky"},
  1160  				},
  1161  				{
  1162  					id:      "10",
  1163  					started: jsonStarted(now + 10),
  1164  					podInfo: podInfoSuccess,
  1165  					finished: jsonFinished(now+11, true, metadata.Metadata{
  1166  						metadata.JobVersion: "build10",
  1167  					}),
  1168  					passed: []string{"good1", "good2", "flaky"},
  1169  				},
  1170  			},
  1171  			expected: &fakeUpload{
  1172  				Buf: mustGrid(&statepb.Grid{
  1173  					Columns: []*statepb.Column{
  1174  						{
  1175  							Build:   "99",
  1176  							Hint:    "99",
  1177  							Started: float64(now+99) * 1000,
  1178  							Extra:   []string{""},
  1179  						},
  1180  						{
  1181  							Build:   "80",
  1182  							Hint:    "80",
  1183  							Started: float64(now+80) * 1000,
  1184  							Extra:   []string{"build80"},
  1185  						},
  1186  						{
  1187  							Build:   "50",
  1188  							Hint:    "50",
  1189  							Started: float64(now+50) * 1000,
  1190  							Extra:   []string{"build50"},
  1191  						},
  1192  						{
  1193  							Build:   "10",
  1194  							Hint:    "10",
  1195  							Started: float64(now+10) * 1000,
  1196  							Extra:   []string{"build10"},
  1197  						},
  1198  					},
  1199  					Rows: []*statepb.Row{
  1200  						setupRow(
  1201  							&statepb.Row{
  1202  								Name: "build." + overallRow,
  1203  								Id:   "build." + overallRow,
  1204  							},
  1205  							cell{
  1206  								Result:  statuspb.TestStatus_RUNNING,
  1207  								Message: "Build still running...",
  1208  								Icon:    "R",
  1209  							},
  1210  							cell{
  1211  								Result:  statuspb.TestStatus_PASS,
  1212  								Metrics: setElapsed(nil, 1),
  1213  							},
  1214  							cell{
  1215  								Result:  statuspb.TestStatus_FAIL,
  1216  								Metrics: setElapsed(nil, 1),
  1217  							},
  1218  							cell{
  1219  								Result:  statuspb.TestStatus_PASS,
  1220  								Metrics: setElapsed(nil, 1),
  1221  							},
  1222  						),
  1223  						setupRow(
  1224  							&statepb.Row{
  1225  								Name: "build." + podInfoRow,
  1226  								Id:   "build." + podInfoRow,
  1227  							},
  1228  							cell{Result: statuspb.TestStatus_NO_RESULT},
  1229  							podInfoPassCell,
  1230  							podInfoPassCell,
  1231  							podInfoPassCell,
  1232  						),
  1233  						setupRow(
  1234  							&statepb.Row{
  1235  								Name: "flaky",
  1236  								Id:   "flaky",
  1237  							},
  1238  							cell{Result: statuspb.TestStatus_NO_RESULT},
  1239  							cell{Result: statuspb.TestStatus_PASS},
  1240  							cell{
  1241  								Result:  statuspb.TestStatus_FAIL,
  1242  								Message: "flaky",
  1243  								Icon:    "F",
  1244  							},
  1245  							cell{Result: statuspb.TestStatus_PASS},
  1246  						),
  1247  						setupRow(
  1248  							&statepb.Row{
  1249  								Name: "good1",
  1250  								Id:   "good1",
  1251  							},
  1252  							cell{Result: statuspb.TestStatus_NO_RESULT},
  1253  							cell{Result: statuspb.TestStatus_PASS},
  1254  							cell{Result: statuspb.TestStatus_PASS},
  1255  							cell{Result: statuspb.TestStatus_PASS},
  1256  						),
  1257  						setupRow(
  1258  							&statepb.Row{
  1259  								Name: "good2",
  1260  								Id:   "good2",
  1261  							},
  1262  							cell{Result: statuspb.TestStatus_NO_RESULT},
  1263  							cell{Result: statuspb.TestStatus_PASS},
  1264  							cell{Result: statuspb.TestStatus_PASS},
  1265  							cell{Result: statuspb.TestStatus_PASS},
  1266  						),
  1267  					},
  1268  				}),
  1269  				CacheControl: "no-cache",
  1270  				WorldRead:    gcs.DefaultACL,
  1271  				Generation:   1,
  1272  			},
  1273  		},
  1274  		{
  1275  			name:      "do not write when requested",
  1276  			skipWrite: true,
  1277  			group: &configpb.TestGroup{
  1278  				GcsPrefix: "bucket/path/to/build/",
  1279  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  1280  					{
  1281  						ConfigurationValue: "Commit",
  1282  					},
  1283  				},
  1284  			},
  1285  			builds: []fakeBuild{
  1286  				{
  1287  					id:      "99",
  1288  					started: jsonStarted(now + 99),
  1289  				},
  1290  				{
  1291  					id:      "80",
  1292  					started: jsonStarted(now + 80),
  1293  					finished: jsonFinished(now+81, true, metadata.Metadata{
  1294  						metadata.JobVersion: "build80",
  1295  					}),
  1296  					passed: []string{"good1", "good2", "flaky"},
  1297  				},
  1298  				{
  1299  					id:      "50",
  1300  					started: jsonStarted(now + 50),
  1301  					finished: jsonFinished(now+51, false, metadata.Metadata{
  1302  						metadata.JobVersion: "build50",
  1303  					}),
  1304  					passed: []string{"good1", "good2"},
  1305  					failed: []string{"flaky"},
  1306  				},
  1307  				{
  1308  					id:      "10",
  1309  					started: jsonStarted(now + 10),
  1310  					finished: jsonFinished(now+11, true, metadata.Metadata{
  1311  						metadata.JobVersion: "build10",
  1312  					}),
  1313  					passed: []string{"good1", "good2", "flaky"},
  1314  				},
  1315  			},
  1316  		},
  1317  		{
  1318  			name: "recent", // keep columns past the reprocess boundary
  1319  			group: &configpb.TestGroup{
  1320  				GcsPrefix: "bucket/path/to/build/",
  1321  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  1322  					{
  1323  						ConfigurationValue: "Commit",
  1324  					},
  1325  				},
  1326  			},
  1327  			reprocess: 10 * time.Second,
  1328  			builds: []fakeBuild{
  1329  				{
  1330  					id:      "current",
  1331  					started: jsonStarted(now),
  1332  				},
  1333  			},
  1334  			current: &fake.Object{
  1335  				Data: string(mustGrid(&statepb.Grid{
  1336  					Columns: []*statepb.Column{
  1337  						{
  1338  							Build:   "current",
  1339  							Hint:    "should reprocess",
  1340  							Started: float64(now * 1000),
  1341  							Extra:   []string{""},
  1342  						},
  1343  						{
  1344  							Build:   "near boundary",
  1345  							Hint:    "1 should disappear",
  1346  							Started: float64(now-7) * 1000, // allow for 2s of clock drift
  1347  							Extra:   []string{""},
  1348  						},
  1349  						{
  1350  							Build:   "past boundary",
  1351  							Hint:    "boundary+999",
  1352  							Started: float64(now-9)*1000 - 1,
  1353  							Extra:   []string{"keep"},
  1354  						},
  1355  					},
  1356  					Rows: []*statepb.Row{
  1357  						setupRow(
  1358  							&statepb.Row{
  1359  								Name: "build." + overallRow,
  1360  								Id:   "build." + overallRow,
  1361  							},
  1362  							cell{
  1363  								Result:  statuspb.TestStatus_PASS,
  1364  								Message: "old data",
  1365  								Icon:    "should reprocess",
  1366  							},
  1367  							cell{
  1368  								Result:  statuspb.TestStatus_FAIL,
  1369  								Message: "delete me",
  1370  								Icon:    "me too",
  1371  							},
  1372  							cell{
  1373  								Result:  statuspb.TestStatus_FLAKY,
  1374  								Message: "keep me",
  1375  								Icon:    "yes stay",
  1376  							},
  1377  						),
  1378  					},
  1379  				})),
  1380  			},
  1381  			expected: &fakeUpload{
  1382  				Buf: mustGrid(&statepb.Grid{
  1383  					Columns: []*statepb.Column{
  1384  						{
  1385  							Build:   "current",
  1386  							Hint:    "current",
  1387  							Started: float64(now) * 1000,
  1388  							Extra:   []string{""},
  1389  						},
  1390  						{
  1391  							Build:   "past boundary",
  1392  							Hint:    "boundary+999",
  1393  							Started: float64(now-9)*1000 - 1,
  1394  							Extra:   []string{"keep"},
  1395  						},
  1396  					},
  1397  					Rows: []*statepb.Row{
  1398  						setupRow(
  1399  							&statepb.Row{
  1400  								Name: "build." + overallRow,
  1401  								Id:   "build." + overallRow,
  1402  							},
  1403  							cell{
  1404  								Result:  statuspb.TestStatus_RUNNING,
  1405  								Message: "Build still running...",
  1406  								Icon:    "R",
  1407  							},
  1408  							cell{
  1409  								Result:  statuspb.TestStatus_FLAKY,
  1410  								Message: "keep me",
  1411  								Icon:    "yes stay",
  1412  							},
  1413  						),
  1414  					},
  1415  				}),
  1416  				CacheControl: "no-cache",
  1417  				WorldRead:    gcs.DefaultACL,
  1418  				Generation:   1,
  1419  			},
  1420  		},
  1421  		{
  1422  			name: "skip reprocess",
  1423  			group: &configpb.TestGroup{
  1424  				GcsPrefix: "bucket/path/to/build/",
  1425  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  1426  					{
  1427  						ConfigurationValue: "Commit",
  1428  					},
  1429  				},
  1430  			},
  1431  			reprocess: 10 * time.Second,
  1432  			builds: []fakeBuild{
  1433  				{
  1434  					id:      "current",
  1435  					started: jsonStarted(now),
  1436  				},
  1437  			},
  1438  			current: &fake.Object{
  1439  				Data: string(mustGrid(&statepb.Grid{
  1440  					Columns: []*statepb.Column{
  1441  						{
  1442  							Build:   "current",
  1443  							Hint:    "should reprocess",
  1444  							Started: float64(now * 1000),
  1445  							Extra:   []string{""},
  1446  						},
  1447  						{
  1448  							Build:   "at boundary",
  1449  							Hint:    "1 should disappear",
  1450  							Started: float64(now-9) * 1000,
  1451  							Extra:   []string{""},
  1452  						},
  1453  						{
  1454  							Build:   "past boundary",
  1455  							Hint:    "1 running",
  1456  							Started: float64(now-40) * 1000,
  1457  							Extra:   []string{""},
  1458  						},
  1459  						{
  1460  							Build:   "paster boundary",
  1461  							Hint:    "1 maybe fix",
  1462  							Started: float64(now-50) * 1000,
  1463  							Extra:   []string{""},
  1464  						},
  1465  						{
  1466  							Build:   "pastest boundary",
  1467  							Hint:    "1 oldest",
  1468  							Started: float64(now-60) * 1000,
  1469  							Extra:   []string{"keep"},
  1470  						},
  1471  					},
  1472  					Rows: []*statepb.Row{
  1473  						setupRow(
  1474  							&statepb.Row{
  1475  								Name: "build." + overallRow,
  1476  								Id:   "build." + overallRow,
  1477  							},
  1478  							cell{
  1479  								Result:  statuspb.TestStatus_PASS,
  1480  								Message: "old data",
  1481  								Icon:    "should reprocess",
  1482  							},
  1483  							cell{
  1484  								Result:  statuspb.TestStatus_FAIL,
  1485  								Message: "delete me",
  1486  								Icon:    "me too",
  1487  							},
  1488  							cell{
  1489  								Result:  statuspb.TestStatus_RUNNING,
  1490  								Message: "delete me",
  1491  								Icon:    "me too",
  1492  							},
  1493  							cell{
  1494  								Result:  statuspb.TestStatus_FLAKY,
  1495  								Message: "maybe reprocess",
  1496  								Icon:    "?",
  1497  							},
  1498  							cell{
  1499  								Result:  statuspb.TestStatus_PASS,
  1500  								Message: "keep me",
  1501  								Icon:    "yes stay",
  1502  							},
  1503  						),
  1504  					},
  1505  				})),
  1506  			},
  1507  			expected: &fakeUpload{
  1508  				Buf: mustGrid(&statepb.Grid{
  1509  					Columns: []*statepb.Column{
  1510  						{
  1511  							Build:   "current",
  1512  							Hint:    "current",
  1513  							Started: float64(now) * 1000,
  1514  							Extra:   []string{""},
  1515  						},
  1516  						{
  1517  							Build:   "paster boundary",
  1518  							Hint:    "1 maybe fix",
  1519  							Started: float64(now-50) * 1000,
  1520  							Extra:   []string{""},
  1521  						},
  1522  						{
  1523  							Build:   "pastest boundary",
  1524  							Hint:    "1 oldest",
  1525  							Started: float64(now-60) * 1000,
  1526  							Extra:   []string{"keep"},
  1527  						},
  1528  					},
  1529  					Rows: []*statepb.Row{
  1530  						setupRow(
  1531  							&statepb.Row{
  1532  								Name: "build." + overallRow,
  1533  								Id:   "build." + overallRow,
  1534  							},
  1535  							cell{
  1536  								Result:  statuspb.TestStatus_RUNNING,
  1537  								Message: "Build still running...",
  1538  								Icon:    "R",
  1539  							},
  1540  							cell{
  1541  								Result:  statuspb.TestStatus_FLAKY,
  1542  								Message: "maybe reprocess",
  1543  								Icon:    "?",
  1544  							},
  1545  							cell{
  1546  								Result:  statuspb.TestStatus_PASS,
  1547  								Message: "keep me",
  1548  								Icon:    "yes stay",
  1549  							},
  1550  						),
  1551  					},
  1552  				}),
  1553  				CacheControl: "no-cache",
  1554  				WorldRead:    gcs.DefaultACL,
  1555  				Generation:   1,
  1556  			},
  1557  		},
  1558  		{
  1559  			name: "reprocess",
  1560  			group: &configpb.TestGroup{
  1561  				GcsPrefix: "bucket/path/to/build/",
  1562  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  1563  					{
  1564  						ConfigurationValue: "Commit",
  1565  					},
  1566  				},
  1567  				DaysOfResults: 30,
  1568  			},
  1569  			reprocess: 10 * time.Second,
  1570  			builds: []fakeBuild{
  1571  				{
  1572  					id:      "current",
  1573  					started: jsonStarted(now),
  1574  				},
  1575  			},
  1576  			current: &fake.Object{
  1577  				Data: string(mustGrid(&statepb.Grid{
  1578  					Config: &configpb.TestGroup{
  1579  						GcsPrefix: "bucket/path/to/build/",
  1580  						ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  1581  							{
  1582  								ConfigurationValue: "Commit",
  1583  							},
  1584  						},
  1585  						DaysOfResults: 20,
  1586  					},
  1587  					Columns: []*statepb.Column{
  1588  						{
  1589  							Build:   "current",
  1590  							Hint:    "should reprocess",
  1591  							Started: float64(now * 1000),
  1592  							Extra:   []string{""},
  1593  						},
  1594  						{
  1595  							Build:   "at boundary",
  1596  							Hint:    "1 should disappear",
  1597  							Started: float64(now-9) * 1000,
  1598  							Extra:   []string{""},
  1599  						},
  1600  						{
  1601  							Build:   "past boundary",
  1602  							Hint:    "1 running",
  1603  							Started: float64(now-40) * 1000,
  1604  							Extra:   []string{""},
  1605  						},
  1606  						{
  1607  							Build:   "paster boundary",
  1608  							Hint:    "1 maybe fix",
  1609  							Started: float64(now-50-dayAgo) * 1000,
  1610  							Extra:   []string{""},
  1611  						},
  1612  						{
  1613  							Build:   "pastest boundary",
  1614  							Hint:    "1 oldest",
  1615  							Started: float64(now-60-10*dayAgo) * 1000,
  1616  							Extra:   []string{"keep"},
  1617  						},
  1618  					},
  1619  					Rows: []*statepb.Row{
  1620  						setupRow(
  1621  							&statepb.Row{
  1622  								Name: "build." + overallRow,
  1623  								Id:   "build." + overallRow,
  1624  							},
  1625  							cell{
  1626  								Result:  statuspb.TestStatus_PASS,
  1627  								Message: "old data",
  1628  								Icon:    "should reprocess",
  1629  							},
  1630  							cell{
  1631  								Result:  statuspb.TestStatus_FAIL,
  1632  								Message: "delete me",
  1633  								Icon:    "me too",
  1634  							},
  1635  							cell{
  1636  								Result:  statuspb.TestStatus_RUNNING,
  1637  								Message: "delete me",
  1638  								Icon:    "me too",
  1639  							},
  1640  							cell{
  1641  								Result:  statuspb.TestStatus_FLAKY,
  1642  								Message: "maybe reprocess",
  1643  								Icon:    "?",
  1644  							},
  1645  							cell{
  1646  								Result:  statuspb.TestStatus_PASS,
  1647  								Message: "keep me",
  1648  								Icon:    "yes stay",
  1649  							},
  1650  						),
  1651  					},
  1652  				})),
  1653  			},
  1654  			expected: &fakeUpload{
  1655  				Buf: mustGrid(&statepb.Grid{
  1656  					Columns: []*statepb.Column{
  1657  						{
  1658  							Build:   "current",
  1659  							Hint:    "current",
  1660  							Started: float64(now) * 1000,
  1661  							Extra:   []string{""},
  1662  						},
  1663  						{
  1664  							Build:   "pastest boundary",
  1665  							Hint:    "1 oldest",
  1666  							Started: float64(now-60-10*dayAgo) * 1000,
  1667  							Extra:   []string{"keep"},
  1668  						},
  1669  					},
  1670  					Rows: []*statepb.Row{
  1671  						setupRow(
  1672  							&statepb.Row{
  1673  								Name: "build." + overallRow,
  1674  								Id:   "build." + overallRow,
  1675  							},
  1676  							cell{
  1677  								Result:  statuspb.TestStatus_RUNNING,
  1678  								Message: "Build still running...",
  1679  								Icon:    "R",
  1680  							},
  1681  							cell{
  1682  								Result:  statuspb.TestStatus_PASS,
  1683  								Message: "keep me",
  1684  								Icon:    "yes stay",
  1685  							},
  1686  						),
  1687  					},
  1688  				}),
  1689  				CacheControl: "no-cache",
  1690  				WorldRead:    gcs.DefaultACL,
  1691  				Generation:   1,
  1692  			},
  1693  		},
  1694  		{
  1695  			// short reprocessing time depends on our reprocessing running columns outside this timeframe.
  1696  			name: "running", // reprocess everything at least as new as the running column
  1697  			group: &configpb.TestGroup{
  1698  				GcsPrefix: "bucket/path/to/build/",
  1699  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  1700  					{
  1701  						ConfigurationValue: "Commit",
  1702  					},
  1703  				},
  1704  			},
  1705  			reprocess: 10 * time.Second,
  1706  			builds: []fakeBuild{
  1707  				{
  1708  					id:      "current",
  1709  					started: jsonStarted(now),
  1710  				},
  1711  			},
  1712  			current: &fake.Object{
  1713  				Data: string(mustGrid(&statepb.Grid{
  1714  					Columns: []*statepb.Column{
  1715  						{
  1716  							Build:   "current",
  1717  							Hint:    "should reprocess",
  1718  							Started: float64(now * 1000),
  1719  							Extra:   []string{""},
  1720  						},
  1721  						{
  1722  							Build:   "at boundary",
  1723  							Hint:    "1 should disappear",
  1724  							Started: float64(now-9) * 1000,
  1725  							Extra:   []string{""},
  1726  						},
  1727  						{
  1728  							Build:   "past boundary",
  1729  							Hint:    "1 done but still reprocess",
  1730  							Started: float64(now-40) * 1000,
  1731  							Extra:   []string{""},
  1732  						},
  1733  						{
  1734  							Build:   "paster boundary",
  1735  							Hint:    "1 running",
  1736  							Started: float64(now-50) * 1000,
  1737  							Extra:   []string{""},
  1738  						},
  1739  						{
  1740  							Build:   "pastest boundary",
  1741  							Hint:    "1 oldest",
  1742  							Started: float64(now-60) * 1000,
  1743  							Extra:   []string{"keep"},
  1744  						},
  1745  					},
  1746  					Rows: []*statepb.Row{
  1747  						setupRow(
  1748  							&statepb.Row{
  1749  								Name: "build." + overallRow,
  1750  								Id:   "build." + overallRow,
  1751  							},
  1752  							cell{
  1753  								Result:  statuspb.TestStatus_PASS,
  1754  								Message: "old data",
  1755  								Icon:    "should reprocess",
  1756  							},
  1757  							cell{
  1758  								Result:  statuspb.TestStatus_FAIL,
  1759  								Message: "delete me",
  1760  								Icon:    "me too",
  1761  							},
  1762  							cell{
  1763  								Result:  statuspb.TestStatus_FAIL,
  1764  								Message: "delete me",
  1765  								Icon:    "me too",
  1766  							},
  1767  							cell{
  1768  								Result:  statuspb.TestStatus_RUNNING,
  1769  								Message: "delete me",
  1770  								Icon:    "me too",
  1771  							},
  1772  							cell{
  1773  								Result:  statuspb.TestStatus_FLAKY,
  1774  								Message: "keep me",
  1775  								Icon:    "yes stay",
  1776  							},
  1777  						),
  1778  					},
  1779  				})),
  1780  			},
  1781  			expected: &fakeUpload{
  1782  				Buf: mustGrid(&statepb.Grid{
  1783  					Columns: []*statepb.Column{
  1784  						{
  1785  							Build:   "current",
  1786  							Hint:    "current",
  1787  							Started: float64(now) * 1000,
  1788  							Extra:   []string{""},
  1789  						},
  1790  						{
  1791  							Build:   "pastest boundary",
  1792  							Hint:    "1 oldest",
  1793  							Started: float64(now-60) * 1000,
  1794  							Extra:   []string{"keep"},
  1795  						},
  1796  					},
  1797  					Rows: []*statepb.Row{
  1798  						setupRow(
  1799  							&statepb.Row{
  1800  								Name: "build." + overallRow,
  1801  								Id:   "build." + overallRow,
  1802  							},
  1803  							cell{
  1804  								Result:  statuspb.TestStatus_RUNNING,
  1805  								Message: "Build still running...",
  1806  								Icon:    "R",
  1807  							},
  1808  							cell{
  1809  								Result:  statuspb.TestStatus_FLAKY,
  1810  								Message: "keep me",
  1811  								Icon:    "yes stay",
  1812  							},
  1813  						),
  1814  					},
  1815  				}),
  1816  				CacheControl: "no-cache",
  1817  				WorldRead:    gcs.DefaultACL,
  1818  				Generation:   1,
  1819  			},
  1820  		},
  1821  		{
  1822  			name: "ignore empty", // ignore builds with no results.
  1823  			group: &configpb.TestGroup{
  1824  				GcsPrefix:              "bucket/path/to/build/",
  1825  				DisableProwjobAnalysis: true,
  1826  			},
  1827  			reprocess: 10 * time.Second,
  1828  			colReader: fakeColReader,
  1829  			builds: []fakeBuild{
  1830  				{
  1831  					id:       "cool5",
  1832  					started:  jsonStarted(now - 10),
  1833  					finished: jsonFinished(now-9, true, metadata.Metadata{}),
  1834  					passed:   []string{"a-test"},
  1835  				},
  1836  				{
  1837  					id:       "empty4",
  1838  					started:  jsonStarted(now - 20),
  1839  					finished: jsonFinished(now-19, true, metadata.Metadata{}),
  1840  				},
  1841  				{
  1842  					id:       "empty3",
  1843  					started:  jsonStarted(now - 30),
  1844  					finished: jsonFinished(now-29, true, metadata.Metadata{}),
  1845  				},
  1846  				{
  1847  					id:       "rad2",
  1848  					started:  jsonStarted(now - 40),
  1849  					finished: jsonFinished(now-39, true, metadata.Metadata{}),
  1850  					passed:   []string{"a-test"},
  1851  				},
  1852  				{
  1853  					id:       "empty1",
  1854  					started:  jsonStarted(now - 50),
  1855  					finished: jsonFinished(now-49, true, metadata.Metadata{}),
  1856  				},
  1857  			},
  1858  			current: &fake.Object{
  1859  				Data: string(mustGrid(&statepb.Grid{
  1860  					Columns: []*statepb.Column{},
  1861  					Rows:    []*statepb.Row{},
  1862  				})),
  1863  			},
  1864  			expected: &fakeUpload{
  1865  				Buf: mustGrid(&statepb.Grid{
  1866  					Columns: []*statepb.Column{
  1867  						{
  1868  							Build:   "cool5",
  1869  							Hint:    "cool5",
  1870  							Started: float64((now - 10) * 1000),
  1871  						},
  1872  						{
  1873  							Build:   "rad2",
  1874  							Hint:    "rad2",
  1875  							Started: float64((now - 40) * 1000),
  1876  						},
  1877  						{
  1878  							Build:   "",
  1879  							Hint:    "empty4",
  1880  							Started: float64((now - 50) * 1000),
  1881  						},
  1882  					},
  1883  					Rows: []*statepb.Row{
  1884  						setupRow(
  1885  							&statepb.Row{
  1886  								Name: "a-test",
  1887  								Id:   "a-test",
  1888  							},
  1889  							cell{Result: statuspb.TestStatus_PASS},
  1890  							cell{Result: statuspb.TestStatus_PASS},
  1891  							cell{Result: statuspb.TestStatus_NO_RESULT},
  1892  						),
  1893  					},
  1894  				}),
  1895  				CacheControl: "no-cache",
  1896  				WorldRead:    gcs.DefaultACL,
  1897  				Generation:   1,
  1898  			},
  1899  		},
  1900  		{
  1901  			name: "ignore empty with old columns", // correctly group old and new empty columns
  1902  			group: &configpb.TestGroup{
  1903  				GcsPrefix:              "bucket/path/to/build/",
  1904  				DisableProwjobAnalysis: true,
  1905  			},
  1906  			reprocess: 10 * time.Second,
  1907  			colReader: fakeColReader,
  1908  			builds: []fakeBuild{
  1909  				{
  1910  					id:       "empty9",
  1911  					started:  jsonStarted(now - 10),
  1912  					finished: jsonFinished(now-9, true, metadata.Metadata{}),
  1913  				},
  1914  				{
  1915  					id:       "empty8",
  1916  					started:  jsonStarted(now - 20),
  1917  					finished: jsonFinished(now-19, true, metadata.Metadata{}),
  1918  				},
  1919  				{
  1920  					id:       "wicked7",
  1921  					started:  jsonStarted(now - 30),
  1922  					finished: jsonFinished(now-29, true, metadata.Metadata{}),
  1923  					passed:   []string{"a-test"},
  1924  				},
  1925  				{
  1926  					id:       "empty6",
  1927  					started:  jsonStarted(now - 40),
  1928  					finished: jsonFinished(now-39, true, metadata.Metadata{}),
  1929  				},
  1930  			},
  1931  			current: &fake.Object{
  1932  				Data: string(mustGrid(&statepb.Grid{
  1933  					Columns: []*statepb.Column{
  1934  						{
  1935  							Build:   "cool5",
  1936  							Hint:    "cool5",
  1937  							Started: float64((now - 50) * 1000),
  1938  						},
  1939  						{
  1940  							Build:   "rad2",
  1941  							Hint:    "rad2",
  1942  							Started: float64((now - 80) * 1000),
  1943  						},
  1944  						{
  1945  							Build:   "",
  1946  							Name:    "",
  1947  							Hint:    "empty4",
  1948  							Started: float64((now - 90) * 1000),
  1949  						},
  1950  					},
  1951  					Rows: []*statepb.Row{
  1952  						setupRow(
  1953  							&statepb.Row{
  1954  								Name: "a-test",
  1955  								Id:   "a-test",
  1956  							},
  1957  							cell{Result: statuspb.TestStatus_PASS},
  1958  							cell{Result: statuspb.TestStatus_PASS},
  1959  							cell{Result: statuspb.TestStatus_NO_RESULT},
  1960  						),
  1961  					},
  1962  				})),
  1963  			},
  1964  			expected: &fakeUpload{
  1965  				Buf: mustGrid(&statepb.Grid{
  1966  					Columns: []*statepb.Column{
  1967  						{
  1968  							Build:   "wicked7",
  1969  							Hint:    "wicked7",
  1970  							Started: float64((now - 30) * 1000),
  1971  						},
  1972  						{
  1973  							Build:   "cool5",
  1974  							Hint:    "cool5",
  1975  							Started: float64((now - 50) * 1000),
  1976  						},
  1977  						{
  1978  							Build:   "rad2",
  1979  							Hint:    "rad2",
  1980  							Started: float64((now - 80) * 1000),
  1981  						},
  1982  						{
  1983  							Build:   "",
  1984  							Name:    "",
  1985  							Hint:    "empty9",
  1986  							Started: float64((now - 90) * 1000),
  1987  						},
  1988  					},
  1989  					Rows: []*statepb.Row{
  1990  						setupRow(
  1991  							&statepb.Row{
  1992  								Name: "a-test",
  1993  								Id:   "a-test",
  1994  							},
  1995  							cell{Result: statuspb.TestStatus_PASS},
  1996  							cell{Result: statuspb.TestStatus_PASS},
  1997  							cell{Result: statuspb.TestStatus_PASS},
  1998  							cell{Result: statuspb.TestStatus_NO_RESULT},
  1999  						),
  2000  					},
  2001  				}),
  2002  				CacheControl: "no-cache",
  2003  				WorldRead:    gcs.DefaultACL,
  2004  				Generation:   1,
  2005  			},
  2006  		},
  2007  	}
  2008  
  2009  	for _, tc := range cases {
  2010  		t.Run(tc.name, func(t *testing.T) {
  2011  			if tc.ctx == nil {
  2012  				tc.ctx = context.Background()
  2013  			}
  2014  			ctx, cancel := context.WithCancel(tc.ctx)
  2015  			defer cancel()
  2016  
  2017  			if tc.concurrency == 0 {
  2018  				tc.concurrency = 1
  2019  			}
  2020  			readResult := resultReaderPool(ctx, logrus.WithField("name", tc.name), tc.concurrency)
  2021  			if tc.groupTimeout == nil {
  2022  				tc.groupTimeout = &defaultTimeout
  2023  			}
  2024  			if tc.buildTimeout == nil {
  2025  				tc.buildTimeout = &defaultTimeout
  2026  			}
  2027  
  2028  			client := &fake.ConditionalClient{
  2029  				UploadClient: fake.UploadClient{
  2030  					Uploader: fakeUploader{},
  2031  					Client: fakeClient{
  2032  						Lister: fakeLister{},
  2033  						Opener: fakeOpener{
  2034  							Paths: map[gcs.Path]fake.Object{},
  2035  						},
  2036  					},
  2037  				},
  2038  			}
  2039  
  2040  			if tc.current != nil {
  2041  				client.Opener.Paths[uploadPath] = *tc.current
  2042  			}
  2043  
  2044  			buildsPath := newPathOrDie("gs://" + tc.group.GcsPrefix)
  2045  			fi := client.Lister[buildsPath]
  2046  			for _, build := range addBuilds(&client.Client, buildsPath, tc.builds...) {
  2047  				fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  2048  					Prefix: build.Path.Object(),
  2049  				})
  2050  			}
  2051  			client.Lister[buildsPath] = fi
  2052  
  2053  			colReader := gcsColumnReader(client, *tc.buildTimeout, readResult, false)
  2054  			if tc.colReader != nil {
  2055  				colReader = tc.colReader(tc.builds)
  2056  			}
  2057  			_, err := InflateDropAppend(
  2058  				ctx,
  2059  				logrus.WithField("test", tc.name),
  2060  				client,
  2061  				tc.group,
  2062  				uploadPath,
  2063  				!tc.skipWrite,
  2064  				colReader,
  2065  				tc.reprocess,
  2066  			)
  2067  			switch {
  2068  			case err != nil:
  2069  				if !tc.err {
  2070  					t.Errorf("InflateDropAppend() got unexpected error: %v", err)
  2071  				}
  2072  			case tc.err:
  2073  				t.Error("InflateDropAppend() failed to receive an error")
  2074  			default:
  2075  				expected := fakeUploader{}
  2076  				if tc.expected != nil {
  2077  					expected[uploadPath] = *tc.expected
  2078  				}
  2079  				actual := client.Uploader
  2080  				diff := cmp.Diff(expected, actual, cmp.AllowUnexported(gcs.Path{}, fakeUpload{}), protocmp.Transform())
  2081  				if diff == "" {
  2082  					return
  2083  				}
  2084  				t.Logf("InflateDropAppend() generated a binary diff (-want +got):\n%s", diff)
  2085  				fakeDownloader := fakeOpener{
  2086  					Paths: map[gcs.Path]fake.Object{
  2087  						uploadPath: {Data: string(actual[uploadPath].Buf)},
  2088  					},
  2089  				}
  2090  				actualGrid, _, err := gcs.DownloadGrid(ctx, fakeDownloader, uploadPath)
  2091  				if err != nil {
  2092  					t.Errorf("actual gcs.DownloadGrid() got unexpected error: %v", err)
  2093  				}
  2094  				fakeDownloader.Paths[uploadPath] = fakeObject{Data: string(tc.expected.Buf)}
  2095  				expectedGrid, _, err := gcs.DownloadGrid(ctx, fakeDownloader, uploadPath)
  2096  				if err != nil {
  2097  					t.Errorf("expected gcs.DownloadGrid() got unexpected error: %v", err)
  2098  				}
  2099  				diff = cmp.Diff(expectedGrid, actualGrid, protocmp.Transform())
  2100  				if diff == "" {
  2101  					return
  2102  				}
  2103  				t.Errorf("gcs.DownloadGrid() got unexpected diff (-want +got):\n%s", diff)
  2104  			}
  2105  		})
  2106  	}
  2107  }
  2108  
  2109  func TestFormatStrftime(t *testing.T) {
  2110  	cases := []struct {
  2111  		name string
  2112  		want string
  2113  	}{
  2114  		{
  2115  			name: "basically works",
  2116  			want: "basically works",
  2117  		},
  2118  		{
  2119  			name: "Mon Jan 2 15:04:05",
  2120  			want: "Mon Jan 2 15:04:05",
  2121  		},
  2122  		{
  2123  			name: "python am/pm: %p",
  2124  			want: "python am/pm: PM",
  2125  		},
  2126  		{
  2127  			name: "python year: %Y",
  2128  			want: "python year: 2006",
  2129  		},
  2130  		{
  2131  			name: "python short year: %y",
  2132  			want: "python short year: 06",
  2133  		},
  2134  		{
  2135  			name: "python month: %m",
  2136  			want: "python month: 01",
  2137  		},
  2138  		{
  2139  			name: "python date: %d",
  2140  			want: "python date: 02",
  2141  		},
  2142  		{
  2143  			name: "python 24hr: %H",
  2144  			want: "python 24hr: 15",
  2145  		},
  2146  		{
  2147  			name: "python minutes: %M",
  2148  			want: "python minutes: 04",
  2149  		},
  2150  		{
  2151  			name: "python seconds: %S",
  2152  			want: "python seconds: 05",
  2153  		},
  2154  	}
  2155  
  2156  	for _, tc := range cases {
  2157  		t.Run(tc.name, func(t *testing.T) {
  2158  			if got := FormatStrftime(tc.name); got != tc.want {
  2159  				t.Errorf("formatStrftime(%q) got %q want %q", tc.name, got, tc.want)
  2160  			}
  2161  		})
  2162  	}
  2163  
  2164  }
  2165  
  2166  func TestTruncateGrid(t *testing.T) {
  2167  	addRows := func(n int, skel *Cell) map[string]Cell {
  2168  		if skel == nil {
  2169  			skel = &Cell{}
  2170  		}
  2171  		out := make(map[string]Cell, n)
  2172  		for i := 0; i < n; i++ {
  2173  			skel.ID = fmt.Sprintf("row-%d", i)
  2174  			out[skel.ID] = *skel
  2175  		}
  2176  		return out
  2177  	}
  2178  
  2179  	addCols := func(rows ...map[string]Cell) []InflatedColumn {
  2180  		out := make([]InflatedColumn, 0, len(rows))
  2181  		for i, cells := range rows {
  2182  			id := fmt.Sprintf("col-%d", i)
  2183  			out = append(out, InflatedColumn{
  2184  				Column: &statepb.Column{
  2185  					Name:  id,
  2186  					Build: id,
  2187  				},
  2188  				Cells: cells,
  2189  			})
  2190  		}
  2191  		return out
  2192  	}
  2193  
  2194  	cases := []struct {
  2195  		name    string
  2196  		cols    []InflatedColumn
  2197  		ceiling int
  2198  		want    []InflatedColumn
  2199  	}{
  2200  		{
  2201  			name: "basically works",
  2202  		},
  2203  		{
  2204  			name: "keep first two cols",
  2205  			cols: addCols(
  2206  				addRows(1000, nil),
  2207  				addRows(1000, nil),
  2208  			),
  2209  			want: addCols(
  2210  				addRows(1000, nil),
  2211  				addRows(1000, nil),
  2212  			),
  2213  		},
  2214  		{
  2215  			name: "shrink after second column",
  2216  			cols: addCols(
  2217  				addRows(1000, nil),
  2218  				addRows(1000, nil),
  2219  				addRows(1000, nil),
  2220  				addRows(1000, nil),
  2221  				addRows(1000, nil),
  2222  			),
  2223  			want: addCols(
  2224  				addRows(1000, nil),
  2225  				addRows(1000, nil),
  2226  			),
  2227  		},
  2228  		{
  2229  			name: "honor ceiling",
  2230  			cols: addCols(
  2231  				addRows(1000, nil),
  2232  				addRows(1000, nil),
  2233  				addRows(1000, nil),
  2234  				addRows(1, nil),
  2235  				addRows(1000, nil),
  2236  			),
  2237  			ceiling: 3001,
  2238  			want: addCols(
  2239  				addRows(1000, nil),
  2240  				addRows(1000, nil),
  2241  				addRows(1000, nil),
  2242  				addRows(1, nil),
  2243  			),
  2244  		},
  2245  	}
  2246  
  2247  	for _, tc := range cases {
  2248  		t.Run(tc.name, func(t *testing.T) {
  2249  			got := truncateGrid(tc.cols, tc.ceiling)
  2250  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  2251  				t.Errorf("truncateGrid() got unexpected diff (-want +got):\n%s", diff)
  2252  			}
  2253  		})
  2254  	}
  2255  }
  2256  
  2257  func TestReprocessColumn(t *testing.T) {
  2258  
  2259  	now := time.Now()
  2260  
  2261  	cases := []struct {
  2262  		name string
  2263  		old  *statepb.Grid
  2264  		cfg  *configpb.TestGroup
  2265  		when time.Time
  2266  		want *InflatedColumn
  2267  	}{
  2268  		{
  2269  			name: "empty",
  2270  			old:  &statepb.Grid{},
  2271  		},
  2272  		{
  2273  			name: "same",
  2274  			old: &statepb.Grid{
  2275  				Config: &configpb.TestGroup{Name: "same"},
  2276  			},
  2277  			cfg: &configpb.TestGroup{Name: "same"},
  2278  		},
  2279  		{
  2280  			name: "changed",
  2281  			old: &statepb.Grid{
  2282  				Config: &configpb.TestGroup{Name: "old"},
  2283  			},
  2284  			cfg:  &configpb.TestGroup{Name: "new"},
  2285  			when: now,
  2286  			want: &InflatedColumn{
  2287  				Column: &statepb.Column{
  2288  					Started: float64(now.Unix() * 1000),
  2289  				},
  2290  				Cells: map[string]Cell{
  2291  					"reprocess": {
  2292  						Result: statuspb.TestStatus_RUNNING,
  2293  					},
  2294  				},
  2295  			},
  2296  		},
  2297  	}
  2298  
  2299  	for _, tc := range cases {
  2300  		t.Run(tc.name, func(t *testing.T) {
  2301  			got := reprocessColumn(logrus.WithField("name", tc.name), tc.old, tc.cfg, tc.when)
  2302  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  2303  				t.Errorf("reprocessColumn() got unexpected diff (-want +got):\n%s", diff)
  2304  			}
  2305  		})
  2306  	}
  2307  }
  2308  
  2309  func Test_ShrinkGridInline(t *testing.T) {
  2310  	cases := []struct {
  2311  		name    string
  2312  		ctx     context.Context
  2313  		tg      *configpb.TestGroup
  2314  		cols    []InflatedColumn
  2315  		issues  map[string][]string
  2316  		ceiling int
  2317  
  2318  		want func(*configpb.TestGroup, []InflatedColumn, map[string][]string) *statepb.Grid
  2319  		err  bool
  2320  	}{
  2321  		{
  2322  			name: "basically works",
  2323  			tg:   &configpb.TestGroup{},
  2324  			want: func(*configpb.TestGroup, []InflatedColumn, map[string][]string) *statepb.Grid {
  2325  				return &statepb.Grid{}
  2326  			},
  2327  		},
  2328  		{
  2329  			name: "unchanged",
  2330  			tg:   &configpb.TestGroup{},
  2331  			cols: []InflatedColumn{
  2332  				{
  2333  					Column: &statepb.Column{
  2334  						Name:  "hi",
  2335  						Build: "there",
  2336  					},
  2337  					Cells: map[string]Cell{
  2338  						"cell": {
  2339  							Result:  statuspb.TestStatus_FAIL,
  2340  							Message: "yo",
  2341  						},
  2342  					},
  2343  				},
  2344  				{
  2345  					Column: &statepb.Column{
  2346  						Name:  "two-name",
  2347  						Build: "two-build",
  2348  					},
  2349  					Cells: map[string]Cell{
  2350  						"cell": {
  2351  							Result:  statuspb.TestStatus_FAIL,
  2352  							Message: "yo",
  2353  						},
  2354  						"two": {
  2355  							Result: statuspb.TestStatus_PASS,
  2356  							Icon:   "S",
  2357  						},
  2358  					},
  2359  				},
  2360  			},
  2361  			want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid {
  2362  				return constructGridFromGroupConfig(logrus.New(), tg, cols, issues)
  2363  			},
  2364  		},
  2365  		{
  2366  			name: "truncate column data",
  2367  			tg:   &configpb.TestGroup{},
  2368  			cols: []InflatedColumn{
  2369  				{
  2370  					Column: &statepb.Column{
  2371  						Name:  "hi",
  2372  						Build: "there",
  2373  					},
  2374  					Cells: map[string]Cell{
  2375  						"cell": {
  2376  							Result:  statuspb.TestStatus_FAIL,
  2377  							Message: "yo",
  2378  						},
  2379  					},
  2380  				},
  2381  				{
  2382  					Column: &statepb.Column{
  2383  						Name:  "two-name",
  2384  						Build: "two-build",
  2385  					},
  2386  					Cells: func() map[string]Cell {
  2387  						cells := map[string]Cell{}
  2388  
  2389  						for i := 0; i < 1000; i++ {
  2390  							cells[fmt.Sprintf("cell-%d", i)] = Cell{
  2391  								Result:  statuspb.TestStatus_FAIL,
  2392  								Message: "yo",
  2393  							}
  2394  						}
  2395  						return cells
  2396  					}(),
  2397  				},
  2398  				{
  2399  					Column: &statepb.Column{
  2400  						Name:  "three-name",
  2401  						Build: "three-build",
  2402  					},
  2403  					Cells: map[string]Cell{
  2404  						"cell": {
  2405  							Result:  statuspb.TestStatus_FAIL,
  2406  							Message: "yo",
  2407  						},
  2408  					},
  2409  				},
  2410  			},
  2411  			ceiling: 200,
  2412  			want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid {
  2413  				expect := []InflatedColumn{
  2414  					{
  2415  						Column: &statepb.Column{
  2416  							Name:  "hi",
  2417  							Build: "there",
  2418  						},
  2419  						Cells: map[string]Cell{
  2420  							"cell": {
  2421  								Result:  statuspb.TestStatus_FAIL,
  2422  								Message: "yo",
  2423  							},
  2424  						},
  2425  					},
  2426  				}
  2427  				logger := logrus.New()
  2428  				grid := constructGridFromGroupConfig(logger, tg, cols, issues)
  2429  				buf, _ := gcs.MarshalGrid(grid)
  2430  				orig := len(buf)
  2431  
  2432  				truncateLastColumn(expect, orig, 200, "byte")
  2433  
  2434  				return constructGridFromGroupConfig(logger, tg, expect, issues)
  2435  			},
  2436  		},
  2437  		{
  2438  			name: "truncate sparse column data",
  2439  			tg:   &configpb.TestGroup{},
  2440  			cols: []InflatedColumn{
  2441  				{
  2442  					Column: &statepb.Column{
  2443  						Name:  "one",
  2444  						Build: "one",
  2445  					},
  2446  					Cells: map[string]Cell{
  2447  						"row-0": {
  2448  							Result: statuspb.TestStatus_FAIL,
  2449  						},
  2450  						"row-1": {
  2451  							Result: statuspb.TestStatus_NO_RESULT,
  2452  						},
  2453  						"row-2": {
  2454  							Result: statuspb.TestStatus_NO_RESULT,
  2455  						},
  2456  					},
  2457  				},
  2458  				{
  2459  					Column: &statepb.Column{
  2460  						Name:  "two",
  2461  						Build: "two",
  2462  					},
  2463  					Cells: func() map[string]Cell {
  2464  						cells := map[string]Cell{}
  2465  
  2466  						for i := 0; i < 100; i++ {
  2467  							cells[fmt.Sprintf("row-%d", i)] = Cell{
  2468  								Result: statuspb.TestStatus_FAIL,
  2469  							}
  2470  						}
  2471  						return cells
  2472  					}(),
  2473  				},
  2474  				{
  2475  					Column: &statepb.Column{
  2476  						Name:  "three",
  2477  						Build: "three",
  2478  					},
  2479  					Cells: map[string]Cell{
  2480  						"row-0": {
  2481  							Result: statuspb.TestStatus_FAIL,
  2482  						},
  2483  						"row-1": {
  2484  							Result: statuspb.TestStatus_FAIL,
  2485  						},
  2486  						"row-2": {
  2487  							Result: statuspb.TestStatus_FAIL,
  2488  						},
  2489  					},
  2490  				},
  2491  			},
  2492  			ceiling: 200,
  2493  			want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid {
  2494  				expect := []InflatedColumn{
  2495  					// Drop the rows that are empty after trimming (e.g. row-1 and row-2).
  2496  					{
  2497  						Column: &statepb.Column{
  2498  							Name:  "one",
  2499  							Build: "one",
  2500  						},
  2501  						Cells: map[string]Cell{
  2502  							"row-0": {
  2503  								Result: statuspb.TestStatus_FAIL,
  2504  							},
  2505  						},
  2506  					},
  2507  				}
  2508  				logger := logrus.New()
  2509  				grid := constructGridFromGroupConfig(logger, tg, cols, issues)
  2510  				buf, _ := gcs.MarshalGrid(grid)
  2511  				orig := len(buf)
  2512  
  2513  				truncateLastColumn(expect, orig, 200, "byte")
  2514  
  2515  				return constructGridFromGroupConfig(logger, tg, expect, issues)
  2516  			},
  2517  		},
  2518  		{
  2519  			name: "delete most column data",
  2520  			tg:   &configpb.TestGroup{},
  2521  			cols: []InflatedColumn{
  2522  				{
  2523  					Column: &statepb.Column{
  2524  						Name:  "hi",
  2525  						Build: "there",
  2526  					},
  2527  					Cells: map[string]Cell{
  2528  						"cell": {
  2529  							Result:  statuspb.TestStatus_FAIL,
  2530  							Message: "yo",
  2531  						},
  2532  					},
  2533  				},
  2534  			},
  2535  			ceiling: 1, // Too small for even one cell
  2536  			want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid {
  2537  				expect := []InflatedColumn{
  2538  					{
  2539  						Column: &statepb.Column{
  2540  							Name:  "hi",
  2541  							Build: "there",
  2542  						},
  2543  						Cells: map[string]Cell{
  2544  							"Truncated": {
  2545  								Result:  statuspb.TestStatus_UNKNOWN,
  2546  								Message: "The grid is too large to update. Split this testgroup into multiple testgroups.",
  2547  							},
  2548  						},
  2549  					},
  2550  				}
  2551  				return constructGridFromGroupConfig(logrus.New(), tg, expect, nil)
  2552  			},
  2553  		},
  2554  		{
  2555  			name: "cancelled context",
  2556  			ctx: func() context.Context {
  2557  				ctx, cancel := context.WithCancel(context.Background())
  2558  				cancel()
  2559  				ctx.Err()
  2560  				return ctx
  2561  			}(),
  2562  			tg: &configpb.TestGroup{},
  2563  			cols: []InflatedColumn{
  2564  				{
  2565  					Column: &statepb.Column{
  2566  						Name:  "hi",
  2567  						Build: "there",
  2568  					},
  2569  					Cells: map[string]Cell{
  2570  						"cell": {
  2571  							Result:  statuspb.TestStatus_FAIL,
  2572  							Message: "yo",
  2573  						},
  2574  					},
  2575  				},
  2576  				{
  2577  					Column: &statepb.Column{
  2578  						Name:  "two-name",
  2579  						Build: "two-build",
  2580  					},
  2581  					Cells: func() map[string]Cell {
  2582  						cells := map[string]Cell{}
  2583  
  2584  						for i := 0; i < 1000; i++ {
  2585  							cells[fmt.Sprintf("cell-%d", i)] = Cell{
  2586  								Result:  statuspb.TestStatus_FAIL,
  2587  								Message: "yo",
  2588  							}
  2589  						}
  2590  						return cells
  2591  					}(),
  2592  				},
  2593  				{
  2594  					Column: &statepb.Column{
  2595  						Name:  "three-name",
  2596  						Build: "three-build",
  2597  					},
  2598  					Cells: map[string]Cell{
  2599  						"cell": {
  2600  							Result:  statuspb.TestStatus_FAIL,
  2601  							Message: "yo",
  2602  						},
  2603  					},
  2604  				},
  2605  			},
  2606  			ceiling: 100,
  2607  			want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid {
  2608  				logger := logrus.New()
  2609  				return constructGridFromGroupConfig(logger, tg, cols, issues)
  2610  			},
  2611  		},
  2612  		{
  2613  			name: "no ceiling",
  2614  			tg:   &configpb.TestGroup{},
  2615  			cols: []InflatedColumn{
  2616  				{
  2617  					Column: &statepb.Column{
  2618  						Name:  "hi",
  2619  						Build: "there",
  2620  					},
  2621  					Cells: map[string]Cell{
  2622  						"cell": {
  2623  							Result:  statuspb.TestStatus_FAIL,
  2624  							Message: "yo",
  2625  						},
  2626  					},
  2627  				},
  2628  				{
  2629  					Column: &statepb.Column{
  2630  						Name:  "two-name",
  2631  						Build: "two-build",
  2632  					},
  2633  					Cells: func() map[string]Cell {
  2634  						cells := map[string]Cell{}
  2635  
  2636  						for i := 0; i < 1000; i++ {
  2637  							cells[fmt.Sprintf("cell-%d", i)] = Cell{
  2638  								Result:  statuspb.TestStatus_FAIL,
  2639  								Message: "yo",
  2640  							}
  2641  						}
  2642  						return cells
  2643  					}(),
  2644  				},
  2645  				{
  2646  					Column: &statepb.Column{
  2647  						Name:  "three-name",
  2648  						Build: "three-build",
  2649  					},
  2650  					Cells: map[string]Cell{
  2651  						"cell": {
  2652  							Result:  statuspb.TestStatus_FAIL,
  2653  							Message: "yo",
  2654  						},
  2655  					},
  2656  				},
  2657  			},
  2658  			want: func(tg *configpb.TestGroup, cols []InflatedColumn, issues map[string][]string) *statepb.Grid {
  2659  				logger := logrus.New()
  2660  				return constructGridFromGroupConfig(logger, tg, cols, issues)
  2661  			},
  2662  		},
  2663  	}
  2664  
  2665  	for _, tc := range cases {
  2666  		t.Run(tc.name, func(t *testing.T) {
  2667  			if tc.ctx == nil {
  2668  				tc.ctx = context.Background()
  2669  			}
  2670  			want := tc.want(tc.tg, tc.cols, tc.issues)
  2671  			got, buf, err := shrinkGridInline(tc.ctx, logrus.WithField("name", tc.name), tc.tg, tc.cols, tc.issues, tc.ceiling)
  2672  			switch {
  2673  			case err != nil:
  2674  				if !tc.err {
  2675  					t.Errorf("unexpected error: %v", err)
  2676  				}
  2677  			case tc.err:
  2678  				t.Errorf("failed to get an error, got %v", got)
  2679  			default:
  2680  				if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
  2681  					t.Errorf("unexpected grid diff (-want +got):\n%s", diff)
  2682  					return
  2683  				}
  2684  				wantBuf, err := gcs.MarshalGrid(want)
  2685  				if err != nil {
  2686  					t.Fatalf("Failed to marshal grid: %v", err)
  2687  				}
  2688  				if diff := cmp.Diff(wantBuf, buf); diff != "" {
  2689  					t.Errorf("unexpected buf diff (-want +got):\n%s", diff)
  2690  				}
  2691  			}
  2692  		})
  2693  	}
  2694  }
  2695  
  2696  func TestOverrideBuild(t *testing.T) {
  2697  	cases := []struct {
  2698  		name string
  2699  		tg   *configpb.TestGroup
  2700  		cols []InflatedColumn
  2701  		want []InflatedColumn
  2702  	}{
  2703  		{
  2704  			name: "basically works",
  2705  			tg:   &configpb.TestGroup{},
  2706  		},
  2707  		{
  2708  			name: "empty override does not override",
  2709  			tg:   &configpb.TestGroup{},
  2710  			cols: []InflatedColumn{
  2711  				{
  2712  					Column: &statepb.Column{
  2713  						Build:   "hello",
  2714  						Started: 7,
  2715  					},
  2716  					Cells: map[string]Cell{
  2717  						"keep": {ID: "me"},
  2718  					},
  2719  				},
  2720  				{
  2721  					Column: &statepb.Column{
  2722  						Build:   "world",
  2723  						Started: 6,
  2724  					},
  2725  				},
  2726  			},
  2727  			want: []InflatedColumn{
  2728  				{
  2729  					Column: &statepb.Column{
  2730  						Build:   "hello",
  2731  						Started: 7,
  2732  					},
  2733  					Cells: map[string]Cell{
  2734  						"keep": {ID: "me"},
  2735  					},
  2736  				},
  2737  				{
  2738  					Column: &statepb.Column{
  2739  						Build:   "world",
  2740  						Started: 6,
  2741  					},
  2742  				},
  2743  			},
  2744  		},
  2745  		{
  2746  			name: "override with python style",
  2747  			tg: &configpb.TestGroup{
  2748  				BuildOverrideStrftime: "%y-%m-%d (%Y) %H:%M:%S %p",
  2749  			},
  2750  			cols: []InflatedColumn{
  2751  				{
  2752  					Column: &statepb.Column{
  2753  						Build:   "drop",
  2754  						Hint:    "of fruit",
  2755  						Name:    "keep",
  2756  						Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000),
  2757  					},
  2758  					Cells: map[string]Cell{
  2759  						"keep": {ID: "me too"},
  2760  					},
  2761  				},
  2762  			},
  2763  			want: []InflatedColumn{
  2764  				{
  2765  					Column: &statepb.Column{
  2766  						Build:   "21-04-22 (2021) 13:14:15 PM",
  2767  						Hint:    "of fruit",
  2768  						Name:    "keep",
  2769  						Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000),
  2770  					},
  2771  					Cells: map[string]Cell{
  2772  						"keep": {ID: "me too"},
  2773  					},
  2774  				},
  2775  			},
  2776  		},
  2777  		{
  2778  			name: "override with golang format",
  2779  			tg: &configpb.TestGroup{
  2780  				BuildOverrideStrftime: "hello 2006 PM",
  2781  			},
  2782  			cols: []InflatedColumn{
  2783  				{
  2784  					Column: &statepb.Column{
  2785  						Build:   "drop",
  2786  						Hint:    "of fruit",
  2787  						Name:    "keep",
  2788  						Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000),
  2789  					},
  2790  					Cells: map[string]Cell{
  2791  						"keep": {ID: "me too"},
  2792  					},
  2793  				},
  2794  			},
  2795  			want: []InflatedColumn{
  2796  				{
  2797  					Column: &statepb.Column{
  2798  						Build:   "hello 2021 PM",
  2799  						Hint:    "of fruit",
  2800  						Name:    "keep",
  2801  						Started: float64(time.Date(2021, 04, 22, 13, 14, 15, 0, time.Local).Unix() * 1000),
  2802  					},
  2803  					Cells: map[string]Cell{
  2804  						"keep": {ID: "me too"},
  2805  					},
  2806  				},
  2807  			},
  2808  		},
  2809  	}
  2810  
  2811  	for _, tc := range cases {
  2812  		t.Run(tc.name, func(t *testing.T) {
  2813  			overrideBuild(tc.tg, tc.cols)
  2814  			if diff := cmp.Diff(tc.want, tc.cols, protocmp.Transform()); diff != "" {
  2815  				t.Errorf("overrideBuild() got unexpected diff (-want +got):\n%s", diff)
  2816  			}
  2817  		})
  2818  	}
  2819  }
  2820  
  2821  func TestGroupColumns(t *testing.T) {
  2822  	cases := []struct {
  2823  		name string
  2824  		tg   *configpb.TestGroup
  2825  		cols []InflatedColumn
  2826  		want []InflatedColumn
  2827  	}{
  2828  		{
  2829  			name: "basically works",
  2830  		},
  2831  		{
  2832  			name: "single column groups do not change",
  2833  			cols: []InflatedColumn{
  2834  				{
  2835  					Column: &statepb.Column{
  2836  						Build:   "hello",
  2837  						Name:    "world",
  2838  						Started: 7,
  2839  					},
  2840  					Cells: map[string]Cell{
  2841  						"keep": {ID: "me"},
  2842  					},
  2843  				},
  2844  				{
  2845  					Column: &statepb.Column{
  2846  						Build:   "another",
  2847  						Name:    "column",
  2848  						Started: 9,
  2849  					},
  2850  					Cells: map[string]Cell{
  2851  						"also": {ID: "remains"},
  2852  					},
  2853  				},
  2854  			},
  2855  			want: []InflatedColumn{
  2856  				{
  2857  					Column: &statepb.Column{
  2858  						Build:   "hello",
  2859  						Name:    "world",
  2860  						Started: 7,
  2861  					},
  2862  					Cells: map[string]Cell{
  2863  						"keep": {ID: "me"},
  2864  					},
  2865  				},
  2866  				{
  2867  					Column: &statepb.Column{
  2868  						Build:   "another",
  2869  						Name:    "column",
  2870  						Started: 9,
  2871  					},
  2872  					Cells: map[string]Cell{
  2873  						"also": {ID: "remains"},
  2874  					},
  2875  				},
  2876  			},
  2877  		},
  2878  		{
  2879  			name: "group columns with the same build and name",
  2880  			cols: []InflatedColumn{
  2881  				{
  2882  					Column: &statepb.Column{
  2883  						Build:   "same",
  2884  						Name:    "lemming",
  2885  						Hint:    "99",
  2886  						Started: 7,
  2887  						Extra: []string{
  2888  							"first",
  2889  							"",
  2890  							"same",
  2891  							"different",
  2892  						},
  2893  					},
  2894  					Cells: map[string]Cell{
  2895  						"keep": {ID: "me"},
  2896  					},
  2897  				},
  2898  				{
  2899  					Column: &statepb.Column{
  2900  						Build:   "same",
  2901  						Name:    "lemming",
  2902  						Hint:    "100",
  2903  						Started: 9,
  2904  						Extra: []string{
  2905  							"",
  2906  							"second",
  2907  							"same",
  2908  							"changed",
  2909  						},
  2910  					},
  2911  					Cells: map[string]Cell{
  2912  						"also": {ID: "remains"},
  2913  					},
  2914  				},
  2915  			},
  2916  			want: []InflatedColumn{
  2917  				{
  2918  					Column: &statepb.Column{
  2919  						Build:   "same",
  2920  						Name:    "lemming",
  2921  						Started: 7,
  2922  						Hint:    "100",
  2923  						Extra: []string{
  2924  							"first",
  2925  							"second",
  2926  							"same",
  2927  							"*",
  2928  						},
  2929  					},
  2930  					Cells: map[string]Cell{
  2931  						"keep": {ID: "me"},
  2932  						"also": {ID: "remains"},
  2933  					},
  2934  				},
  2935  			},
  2936  		},
  2937  		{
  2938  			name: "group columns with the same build and name, listing all values",
  2939  			tg: &configpb.TestGroup{
  2940  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  2941  					{
  2942  						Property:      "",
  2943  						ListAllValues: true,
  2944  					},
  2945  					{
  2946  						Property:      "",
  2947  						ListAllValues: true,
  2948  					},
  2949  					{
  2950  						Property:      "",
  2951  						ListAllValues: true,
  2952  					},
  2953  					{
  2954  						Property:      "",
  2955  						ListAllValues: true,
  2956  					},
  2957  					{
  2958  						Property:      "",
  2959  						ListAllValues: true,
  2960  					},
  2961  				},
  2962  			},
  2963  			cols: []InflatedColumn{
  2964  				{
  2965  					Column: &statepb.Column{
  2966  						Build:   "same",
  2967  						Name:    "lemming",
  2968  						Hint:    "99",
  2969  						Started: 7,
  2970  						Extra: []string{
  2971  							"first",
  2972  							"",
  2973  							"same",
  2974  							"different",
  2975  							"overlap||some",
  2976  						},
  2977  					},
  2978  					Cells: map[string]Cell{
  2979  						"keep": {ID: "me"},
  2980  					},
  2981  				},
  2982  				{
  2983  					Column: &statepb.Column{
  2984  						Build:   "same",
  2985  						Name:    "lemming",
  2986  						Hint:    "100",
  2987  						Started: 9,
  2988  						Extra: []string{
  2989  							"",
  2990  							"second",
  2991  							"same",
  2992  							"changed",
  2993  							"other||overlap",
  2994  						},
  2995  					},
  2996  					Cells: map[string]Cell{
  2997  						"also": {ID: "remains"},
  2998  					},
  2999  				},
  3000  			},
  3001  			want: []InflatedColumn{
  3002  				{
  3003  					Column: &statepb.Column{
  3004  						Build:   "same",
  3005  						Name:    "lemming",
  3006  						Started: 7,
  3007  						Hint:    "100",
  3008  						Extra: []string{
  3009  							"first",
  3010  							"second",
  3011  							"same",
  3012  							"changed||different",
  3013  							"other||overlap||some",
  3014  						},
  3015  					},
  3016  					Cells: map[string]Cell{
  3017  						"keep": {ID: "me"},
  3018  						"also": {ID: "remains"},
  3019  					},
  3020  				},
  3021  			},
  3022  		},
  3023  		{
  3024  			name: "columns add more headers",
  3025  			tg: &configpb.TestGroup{
  3026  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  3027  					{
  3028  						Property: "",
  3029  					},
  3030  					{
  3031  						Property: "",
  3032  					},
  3033  					{
  3034  						Property: "",
  3035  					},
  3036  				},
  3037  			},
  3038  			cols: []InflatedColumn{
  3039  				{
  3040  					Column: &statepb.Column{
  3041  						Build:   "same",
  3042  						Name:    "lemming",
  3043  						Hint:    "99",
  3044  						Started: 7,
  3045  						Extra: []string{
  3046  							"one",
  3047  						},
  3048  					},
  3049  				},
  3050  				{
  3051  					Column: &statepb.Column{
  3052  						Build:   "same",
  3053  						Name:    "lemming",
  3054  						Hint:    "100",
  3055  						Started: 9,
  3056  						Extra: []string{
  3057  							"one",
  3058  							"two",
  3059  						},
  3060  					},
  3061  				},
  3062  				{
  3063  					Column: &statepb.Column{
  3064  						Build:   "same",
  3065  						Name:    "lemming",
  3066  						Hint:    "100",
  3067  						Started: 9,
  3068  						Extra: []string{
  3069  							"one",
  3070  							"two",
  3071  							"three",
  3072  						},
  3073  					},
  3074  				},
  3075  			},
  3076  			want: []InflatedColumn{
  3077  				{
  3078  					Column: &statepb.Column{
  3079  						Build:   "same",
  3080  						Name:    "lemming",
  3081  						Started: 7,
  3082  						Hint:    "100",
  3083  						Extra: []string{
  3084  							"one",
  3085  							"two",
  3086  							"three",
  3087  						},
  3088  					},
  3089  					Cells: map[string]Cell{},
  3090  				},
  3091  			},
  3092  		},
  3093  		{
  3094  			name: "columns remove headers",
  3095  			tg: &configpb.TestGroup{
  3096  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
  3097  					{
  3098  						Property: "",
  3099  					},
  3100  					{
  3101  						Property: "",
  3102  					},
  3103  					{
  3104  						Property: "",
  3105  					},
  3106  				},
  3107  			},
  3108  			cols: []InflatedColumn{
  3109  				{
  3110  					Column: &statepb.Column{
  3111  						Build:   "same",
  3112  						Name:    "lemming",
  3113  						Hint:    "99",
  3114  						Started: 7,
  3115  						Extra: []string{
  3116  							"one",
  3117  							"two",
  3118  							"three",
  3119  						},
  3120  					},
  3121  				},
  3122  				{
  3123  					Column: &statepb.Column{
  3124  						Build:   "same",
  3125  						Name:    "lemming",
  3126  						Hint:    "100",
  3127  						Started: 9,
  3128  						Extra: []string{
  3129  							"one",
  3130  							"two",
  3131  						},
  3132  					},
  3133  				},
  3134  				{
  3135  					Column: &statepb.Column{
  3136  						Build:   "same",
  3137  						Name:    "lemming",
  3138  						Hint:    "100",
  3139  						Started: 9,
  3140  						Extra: []string{
  3141  							"one",
  3142  						},
  3143  					},
  3144  				},
  3145  			},
  3146  			want: []InflatedColumn{
  3147  				{
  3148  					Column: &statepb.Column{
  3149  						Build:   "same",
  3150  						Name:    "lemming",
  3151  						Started: 7,
  3152  						Hint:    "100",
  3153  						Extra: []string{
  3154  							"one",
  3155  							"two",
  3156  							"three",
  3157  						},
  3158  					},
  3159  					Cells: map[string]Cell{},
  3160  				},
  3161  			},
  3162  		},
  3163  		{
  3164  			name: "do not group different builds",
  3165  			cols: []InflatedColumn{
  3166  				{
  3167  					Column: &statepb.Column{
  3168  						Build:   "this",
  3169  						Name:    "same",
  3170  						Started: 7,
  3171  					},
  3172  					Cells: map[string]Cell{
  3173  						"keep": {ID: "me"},
  3174  					},
  3175  				},
  3176  				{
  3177  					Column: &statepb.Column{
  3178  						Build:   "that",
  3179  						Name:    "same",
  3180  						Started: 9,
  3181  					},
  3182  					Cells: map[string]Cell{
  3183  						"also": {ID: "remains"},
  3184  					},
  3185  				},
  3186  			},
  3187  			want: []InflatedColumn{
  3188  				{
  3189  					Column: &statepb.Column{
  3190  						Build:   "this",
  3191  						Name:    "same",
  3192  						Started: 7,
  3193  					},
  3194  					Cells: map[string]Cell{
  3195  						"keep": {ID: "me"},
  3196  					},
  3197  				},
  3198  				{
  3199  					Column: &statepb.Column{
  3200  						Build:   "that",
  3201  						Name:    "same",
  3202  						Started: 9,
  3203  					},
  3204  					Cells: map[string]Cell{
  3205  						"also": {ID: "remains"},
  3206  					},
  3207  				},
  3208  			},
  3209  		},
  3210  		{
  3211  			name: "do not group different names",
  3212  			cols: []InflatedColumn{
  3213  				{
  3214  					Column: &statepb.Column{
  3215  						Build:   "same",
  3216  						Name:    "different",
  3217  						Started: 7,
  3218  					},
  3219  					Cells: map[string]Cell{
  3220  						"keep": {ID: "me"},
  3221  					},
  3222  				},
  3223  				{
  3224  					Column: &statepb.Column{
  3225  						Build:   "same",
  3226  						Name:    "changed",
  3227  						Started: 9,
  3228  					},
  3229  					Cells: map[string]Cell{
  3230  						"also": {ID: "remains"},
  3231  					},
  3232  				},
  3233  			},
  3234  			want: []InflatedColumn{
  3235  				{
  3236  					Column: &statepb.Column{
  3237  						Build:   "same",
  3238  						Name:    "different",
  3239  						Started: 7,
  3240  					},
  3241  					Cells: map[string]Cell{
  3242  						"keep": {ID: "me"},
  3243  					},
  3244  				},
  3245  				{
  3246  					Column: &statepb.Column{
  3247  						Build:   "same",
  3248  						Name:    "changed",
  3249  						Started: 9,
  3250  					},
  3251  					Cells: map[string]Cell{
  3252  						"also": {ID: "remains"},
  3253  					},
  3254  				},
  3255  			},
  3256  		},
  3257  		{
  3258  			name: "split merged rows with the same name",
  3259  			cols: []InflatedColumn{
  3260  				{
  3261  					Column: &statepb.Column{
  3262  						Build:   "same",
  3263  						Name:    "same",
  3264  						Started: 7,
  3265  					},
  3266  					Cells: map[string]Cell{
  3267  						"first": {ID: "first"},
  3268  						"same":  {ID: "first-different"},
  3269  					},
  3270  				},
  3271  				{
  3272  					Column: &statepb.Column{
  3273  						Build:   "same",
  3274  						Name:    "same",
  3275  						Started: 9,
  3276  					},
  3277  					Cells: map[string]Cell{
  3278  						"same":   {ID: "second-changed"},
  3279  						"second": {ID: "second"},
  3280  					},
  3281  				},
  3282  			},
  3283  			want: []InflatedColumn{
  3284  				{
  3285  					Column: &statepb.Column{
  3286  						Build:   "same",
  3287  						Name:    "same",
  3288  						Started: 7,
  3289  					},
  3290  					Cells: map[string]Cell{
  3291  						"first":    {ID: "first"},
  3292  						"same":     {ID: "first-different"},
  3293  						"same [1]": {ID: "second-changed"},
  3294  						"second":   {ID: "second"},
  3295  					},
  3296  				},
  3297  			},
  3298  		},
  3299  		{
  3300  			name: "ignore_old_results only takes newest",
  3301  			tg: &configpb.TestGroup{
  3302  				IgnoreOldResults: true,
  3303  			},
  3304  			cols: []InflatedColumn{
  3305  				{
  3306  					Column: &statepb.Column{
  3307  						Build:   "same",
  3308  						Name:    "same",
  3309  						Started: 9,
  3310  					},
  3311  					Cells: map[string]Cell{
  3312  						"first": {ID: "first"},
  3313  						"same":  {ID: "first-different"},
  3314  					},
  3315  				},
  3316  				{
  3317  					Column: &statepb.Column{
  3318  						Build:   "same",
  3319  						Name:    "same",
  3320  						Started: 7,
  3321  					},
  3322  					Cells: map[string]Cell{
  3323  						"same":   {ID: "second-changed"},
  3324  						"second": {ID: "second"},
  3325  					},
  3326  				},
  3327  			},
  3328  			want: []InflatedColumn{
  3329  				{
  3330  					Column: &statepb.Column{
  3331  						Build:   "same",
  3332  						Name:    "same",
  3333  						Started: 7,
  3334  					},
  3335  					Cells: map[string]Cell{
  3336  						"first":  {ID: "first"},
  3337  						"same":   {ID: "first-different"},
  3338  						"second": {ID: "second"},
  3339  					},
  3340  				},
  3341  			},
  3342  		},
  3343  	}
  3344  
  3345  	for _, tc := range cases {
  3346  		t.Run(tc.name, func(t *testing.T) {
  3347  			tg := tc.tg
  3348  			if tg == nil {
  3349  				tg = &configpb.TestGroup{}
  3350  			}
  3351  			got := groupColumns(tg, tc.cols)
  3352  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  3353  				t.Errorf("groupColumns() got unexpected diff (-want +got):\n%s", diff)
  3354  			}
  3355  		})
  3356  	}
  3357  }
  3358  
  3359  // TODO(amerai): This is kind of, but not quite, redundant with summary.gridMetrics().
  3360  func TestColumnStats(t *testing.T) {
  3361  	passCell := Cell{Result: statuspb.TestStatus_PASS}
  3362  	failCell := Cell{Result: statuspb.TestStatus_FAIL}
  3363  
  3364  	cases := []struct {
  3365  		name            string
  3366  		cells           map[string]Cell
  3367  		brokenThreshold float32
  3368  		want            *statepb.Stats
  3369  	}{
  3370  		{
  3371  			name:            "nil",
  3372  			brokenThreshold: 0.5,
  3373  			want:            nil,
  3374  		},
  3375  		{
  3376  			name:            "empty",
  3377  			brokenThreshold: 0.5,
  3378  			cells:           map[string]Cell{},
  3379  			want:            &statepb.Stats{},
  3380  		},
  3381  		{
  3382  			name: "nil, no threshold",
  3383  			want: nil,
  3384  		},
  3385  		{
  3386  			name:  "empty, no threshold",
  3387  			cells: map[string]Cell{},
  3388  			want:  nil,
  3389  		},
  3390  		{
  3391  			name: "no threshold",
  3392  			cells: map[string]Cell{
  3393  				"a": passCell,
  3394  				"b": failCell,
  3395  			},
  3396  			want: nil,
  3397  		},
  3398  		{
  3399  			name:            "blank",
  3400  			brokenThreshold: 0.5,
  3401  			cells: map[string]Cell{
  3402  				"a": emptyCell,
  3403  				"b": emptyCell,
  3404  				"c": emptyCell,
  3405  				"d": emptyCell,
  3406  			},
  3407  			want: &statepb.Stats{
  3408  				PassCount:  0,
  3409  				FailCount:  0,
  3410  				TotalCount: 0,
  3411  			},
  3412  		},
  3413  		{
  3414  			name:            "passing",
  3415  			brokenThreshold: 0.5,
  3416  			cells: map[string]Cell{
  3417  				"a": passCell,
  3418  				"b": passCell,
  3419  				"c": passCell,
  3420  				"d": passCell,
  3421  			},
  3422  			want: &statepb.Stats{
  3423  				PassCount:  4,
  3424  				FailCount:  0,
  3425  				TotalCount: 4,
  3426  			},
  3427  		},
  3428  		{
  3429  			name:            "failing",
  3430  			brokenThreshold: 0.5,
  3431  			cells: map[string]Cell{
  3432  				"a": failCell,
  3433  				"b": failCell,
  3434  				"c": failCell,
  3435  				"d": failCell,
  3436  			},
  3437  			want: &statepb.Stats{
  3438  				PassCount:  0,
  3439  				FailCount:  4,
  3440  				TotalCount: 4,
  3441  				Broken:     true,
  3442  			},
  3443  		},
  3444  		{
  3445  			name:            "mix, not broken",
  3446  			brokenThreshold: 0.5,
  3447  			cells: map[string]Cell{
  3448  				"a": passCell,
  3449  				"b": passCell,
  3450  				"c": failCell,
  3451  				"d": passCell,
  3452  			},
  3453  			want: &statepb.Stats{
  3454  				PassCount:  3,
  3455  				FailCount:  1,
  3456  				TotalCount: 4,
  3457  			},
  3458  		},
  3459  		{
  3460  			name:            "mix, broken",
  3461  			brokenThreshold: 0.5,
  3462  			cells: map[string]Cell{
  3463  				"a": failCell,
  3464  				"b": passCell,
  3465  				"c": failCell,
  3466  				"d": failCell,
  3467  			},
  3468  			want: &statepb.Stats{
  3469  				PassCount:  1,
  3470  				FailCount:  3,
  3471  				TotalCount: 4,
  3472  				Broken:     true,
  3473  			},
  3474  		},
  3475  		{
  3476  			name:            "mix, blank cells",
  3477  			brokenThreshold: 0.5,
  3478  			cells: map[string]Cell{
  3479  				"a": failCell,
  3480  				"b": passCell,
  3481  				"c": emptyCell,
  3482  				"d": emptyCell,
  3483  				"e": failCell,
  3484  				"f": passCell,
  3485  				"g": failCell,
  3486  				"h": emptyCell,
  3487  			},
  3488  			want: &statepb.Stats{
  3489  				PassCount:  2,
  3490  				FailCount:  3,
  3491  				TotalCount: 5,
  3492  				Broken:     true,
  3493  			},
  3494  		},
  3495  		{
  3496  			name:            "pending",
  3497  			brokenThreshold: 0.5,
  3498  			cells: map[string]Cell{
  3499  				"a": failCell,
  3500  				"b": passCell,
  3501  				"c": passCell,
  3502  				"d": {Result: statuspb.TestStatus_RUNNING},
  3503  			},
  3504  			want: &statepb.Stats{
  3505  				PassCount:  2,
  3506  				FailCount:  1,
  3507  				TotalCount: 4,
  3508  				Pending:    true,
  3509  			},
  3510  		},
  3511  		{
  3512  			name:            "advanced",
  3513  			brokenThreshold: 0.5,
  3514  			cells: map[string]Cell{
  3515  				"a": failCell,
  3516  				"b": passCell,
  3517  				"c": emptyCell,
  3518  				"d": {Result: statuspb.TestStatus_BLOCKED},
  3519  				"e": {Result: statuspb.TestStatus_BUILD_FAIL},
  3520  				"f": {Result: statuspb.TestStatus_BUILD_PASSED},
  3521  				"g": {Result: statuspb.TestStatus_CANCEL},
  3522  				"h": {Result: statuspb.TestStatus_CATEGORIZED_ABORT},
  3523  				"i": {Result: statuspb.TestStatus_CATEGORIZED_FAIL},
  3524  				"j": {Result: statuspb.TestStatus_FLAKY},
  3525  				"k": {Result: statuspb.TestStatus_PASS_WITH_ERRORS},
  3526  				"l": {Result: statuspb.TestStatus_PASS_WITH_SKIPS},
  3527  				"m": {Result: statuspb.TestStatus_TIMED_OUT},
  3528  				"n": {Result: statuspb.TestStatus_TOOL_FAIL},
  3529  				"o": {Result: statuspb.TestStatus_UNKNOWN},
  3530  				"p": {Result: statuspb.TestStatus_RUNNING},
  3531  			},
  3532  			want: &statepb.Stats{
  3533  				PassCount:  4,
  3534  				FailCount:  5,
  3535  				TotalCount: 15,
  3536  				Pending:    true,
  3537  			},
  3538  		},
  3539  	}
  3540  
  3541  	for _, tc := range cases {
  3542  		t.Run(tc.name, func(t *testing.T) {
  3543  			got := columnStats(tc.cells, tc.brokenThreshold)
  3544  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  3545  				t.Errorf("columnStats(%v, %f) got unexpected diff (-want +got):\n%s", tc.cells, tc.brokenThreshold, diff)
  3546  			}
  3547  		})
  3548  	}
  3549  }
  3550  
  3551  func TestConstructGrid(t *testing.T) {
  3552  	defaultCustomColumnHeaders := []*configpb.TestGroup_ColumnHeader{
  3553  		{
  3554  			Property:           "hello",
  3555  			ConfigurationValue: "world!",
  3556  		},
  3557  		{
  3558  			Property:           "foo",
  3559  			ConfigurationValue: "bar",
  3560  		},
  3561  	}
  3562  	cases := []struct {
  3563  		name                    string
  3564  		cols                    []inflatedColumn
  3565  		numFailuresToAlert      int
  3566  		numPassesToDisableAlert int
  3567  		issues                  map[string][]string
  3568  		brokenThreshold         float32
  3569  		columnHeader            []*configpb.TestGroup_ColumnHeader
  3570  		expected                *statepb.Grid
  3571  	}{
  3572  		{
  3573  			name:     "basically works",
  3574  			expected: &statepb.Grid{},
  3575  		},
  3576  		{
  3577  			name:         "multiple columns",
  3578  			columnHeader: defaultCustomColumnHeaders,
  3579  			cols: []inflatedColumn{
  3580  				{
  3581  					Column: &statepb.Column{Build: "15"},
  3582  					Cells: map[string]cell{
  3583  						"green": {
  3584  							Result: statuspb.TestStatus_PASS,
  3585  						},
  3586  						"red": {
  3587  							Result: statuspb.TestStatus_FAIL,
  3588  						},
  3589  						"only-15": {
  3590  							Result: statuspb.TestStatus_FLAKY,
  3591  						},
  3592  					},
  3593  				},
  3594  				{
  3595  					Column: &statepb.Column{Build: "10"},
  3596  					Cells: map[string]cell{
  3597  						"full": {
  3598  							Result:  statuspb.TestStatus_PASS,
  3599  							CellID:  "cell",
  3600  							Icon:    "icon",
  3601  							Message: "message",
  3602  							Metrics: map[string]float64{
  3603  								"elapsed": 1,
  3604  								"keys":    2,
  3605  							},
  3606  							UserProperty: "food",
  3607  						},
  3608  						"green": {
  3609  							Result: statuspb.TestStatus_PASS,
  3610  						},
  3611  						"red": {
  3612  							Result: statuspb.TestStatus_FAIL,
  3613  						},
  3614  						"only-10": {
  3615  							Result: statuspb.TestStatus_FLAKY,
  3616  						},
  3617  					},
  3618  				},
  3619  			},
  3620  			expected: &statepb.Grid{
  3621  				Columns: []*statepb.Column{
  3622  					{Build: "15"},
  3623  					{Build: "10"},
  3624  				},
  3625  				Rows: []*statepb.Row{
  3626  					setupRow(
  3627  						&statepb.Row{
  3628  							Name:         "full",
  3629  							Id:           "full",
  3630  							UserProperty: []string{},
  3631  						},
  3632  						emptyCell,
  3633  						cell{
  3634  							Result:  statuspb.TestStatus_PASS,
  3635  							CellID:  "cell",
  3636  							Icon:    "icon",
  3637  							Message: "message",
  3638  							Metrics: map[string]float64{
  3639  								"elapsed": 1,
  3640  								"keys":    2,
  3641  							},
  3642  							UserProperty: "food",
  3643  						},
  3644  					),
  3645  					setupRow(
  3646  						&statepb.Row{
  3647  							Name: "green",
  3648  							Id:   "green",
  3649  						},
  3650  						cell{Result: statuspb.TestStatus_PASS},
  3651  						cell{Result: statuspb.TestStatus_PASS},
  3652  					),
  3653  					setupRow(
  3654  						&statepb.Row{
  3655  							Name: "only-10",
  3656  							Id:   "only-10",
  3657  						},
  3658  						emptyCell,
  3659  						cell{Result: statuspb.TestStatus_FLAKY},
  3660  					),
  3661  					setupRow(
  3662  						&statepb.Row{
  3663  							Name: "only-15",
  3664  							Id:   "only-15",
  3665  						},
  3666  						cell{Result: statuspb.TestStatus_FLAKY},
  3667  						emptyCell,
  3668  					),
  3669  					setupRow(
  3670  						&statepb.Row{
  3671  							Name: "red",
  3672  							Id:   "red",
  3673  						},
  3674  						cell{Result: statuspb.TestStatus_FAIL},
  3675  						cell{Result: statuspb.TestStatus_FAIL},
  3676  					),
  3677  				},
  3678  			},
  3679  		},
  3680  		{
  3681  			name:            "multiple columns with threshold",
  3682  			columnHeader:    defaultCustomColumnHeaders,
  3683  			brokenThreshold: 0.3,
  3684  			cols: []inflatedColumn{
  3685  				{
  3686  					Column: &statepb.Column{Build: "15"},
  3687  					Cells: map[string]cell{
  3688  						"green": {
  3689  							Result: statuspb.TestStatus_PASS,
  3690  						},
  3691  						"red": {
  3692  							Result: statuspb.TestStatus_FAIL,
  3693  						},
  3694  						"only-15": {
  3695  							Result: statuspb.TestStatus_FLAKY,
  3696  						},
  3697  					},
  3698  				},
  3699  				{
  3700  					Column: &statepb.Column{Build: "10"},
  3701  					Cells: map[string]cell{
  3702  						"full": {
  3703  							Result:  statuspb.TestStatus_PASS,
  3704  							CellID:  "cell",
  3705  							Icon:    "icon",
  3706  							Message: "message",
  3707  							Metrics: map[string]float64{
  3708  								"elapsed": 1,
  3709  								"keys":    2,
  3710  							},
  3711  							UserProperty: "food",
  3712  						},
  3713  						"green": {
  3714  							Result: statuspb.TestStatus_PASS,
  3715  						},
  3716  						"red": {
  3717  							Result: statuspb.TestStatus_FAIL,
  3718  						},
  3719  						"only-10": {
  3720  							Result: statuspb.TestStatus_FLAKY,
  3721  						},
  3722  					},
  3723  				},
  3724  			},
  3725  			expected: &statepb.Grid{
  3726  				Columns: []*statepb.Column{
  3727  					{
  3728  						Build: "15",
  3729  						Stats: &statepb.Stats{
  3730  							FailCount:  1,
  3731  							PassCount:  1,
  3732  							TotalCount: 3,
  3733  							Broken:     true,
  3734  						},
  3735  					},
  3736  					{
  3737  						Build: "10",
  3738  						Stats: &statepb.Stats{
  3739  							FailCount:  1,
  3740  							PassCount:  2,
  3741  							TotalCount: 4,
  3742  						},
  3743  					},
  3744  				},
  3745  				Rows: []*statepb.Row{
  3746  					setupRow(
  3747  						&statepb.Row{
  3748  							Name:         "full",
  3749  							Id:           "full",
  3750  							UserProperty: []string{},
  3751  						},
  3752  						emptyCell,
  3753  						cell{
  3754  							Result:  statuspb.TestStatus_PASS,
  3755  							CellID:  "cell",
  3756  							Icon:    "icon",
  3757  							Message: "message",
  3758  							Metrics: map[string]float64{
  3759  								"elapsed": 1,
  3760  								"keys":    2,
  3761  							},
  3762  							UserProperty: "food",
  3763  						},
  3764  					),
  3765  					setupRow(
  3766  						&statepb.Row{
  3767  							Name: "green",
  3768  							Id:   "green",
  3769  						},
  3770  						cell{Result: statuspb.TestStatus_PASS},
  3771  						cell{Result: statuspb.TestStatus_PASS},
  3772  					),
  3773  					setupRow(
  3774  						&statepb.Row{
  3775  							Name: "only-10",
  3776  							Id:   "only-10",
  3777  						},
  3778  						emptyCell,
  3779  						cell{Result: statuspb.TestStatus_FLAKY},
  3780  					),
  3781  					setupRow(
  3782  						&statepb.Row{
  3783  							Name: "only-15",
  3784  							Id:   "only-15",
  3785  						},
  3786  						cell{Result: statuspb.TestStatus_FLAKY},
  3787  						emptyCell,
  3788  					),
  3789  					setupRow(
  3790  						&statepb.Row{
  3791  							Name: "red",
  3792  							Id:   "red",
  3793  						},
  3794  						cell{Result: statuspb.TestStatus_FAIL},
  3795  						cell{Result: statuspb.TestStatus_FAIL},
  3796  					),
  3797  				},
  3798  			},
  3799  		},
  3800  		{
  3801  			name:                    "open alert",
  3802  			numFailuresToAlert:      2,
  3803  			numPassesToDisableAlert: 2,
  3804  			cols: []inflatedColumn{
  3805  				{
  3806  					Column: &statepb.Column{Build: "4"},
  3807  					Cells: map[string]cell{
  3808  						"just-flaky": {
  3809  							Result: statuspb.TestStatus_FAIL,
  3810  						},
  3811  						"broken": {
  3812  							Result: statuspb.TestStatus_FAIL,
  3813  						},
  3814  					},
  3815  				},
  3816  				{
  3817  					Column: &statepb.Column{Build: "3"},
  3818  					Cells: map[string]cell{
  3819  						"just-flaky": {
  3820  							Result: statuspb.TestStatus_PASS,
  3821  						},
  3822  						"broken": {
  3823  							Result: statuspb.TestStatus_FAIL,
  3824  						},
  3825  					},
  3826  				},
  3827  			},
  3828  			expected: &statepb.Grid{
  3829  				Columns: []*statepb.Column{
  3830  					{Build: "4"},
  3831  					{Build: "3"},
  3832  				},
  3833  				Rows: []*statepb.Row{
  3834  					setupRow(
  3835  						&statepb.Row{
  3836  							Name: "broken",
  3837  							Id:   "broken",
  3838  						},
  3839  						cell{Result: statuspb.TestStatus_FAIL},
  3840  						cell{Result: statuspb.TestStatus_FAIL},
  3841  					),
  3842  					setupRow(
  3843  						&statepb.Row{
  3844  							Name: "just-flaky",
  3845  							Id:   "just-flaky",
  3846  						},
  3847  						cell{Result: statuspb.TestStatus_FAIL},
  3848  						cell{Result: statuspb.TestStatus_PASS},
  3849  					),
  3850  				},
  3851  			},
  3852  		},
  3853  		{
  3854  			name:                    "close alert",
  3855  			numFailuresToAlert:      1,
  3856  			numPassesToDisableAlert: 2,
  3857  			cols: []inflatedColumn{
  3858  				{
  3859  					Column: &statepb.Column{Build: "4"},
  3860  					Cells: map[string]cell{
  3861  						"still-broken": {
  3862  							Result: statuspb.TestStatus_PASS,
  3863  						},
  3864  						"fixed": {
  3865  							Result: statuspb.TestStatus_PASS,
  3866  						},
  3867  					},
  3868  				},
  3869  				{
  3870  					Column: &statepb.Column{Build: "3"},
  3871  					Cells: map[string]cell{
  3872  						"still-broken": {
  3873  							Result: statuspb.TestStatus_FAIL,
  3874  						},
  3875  						"fixed": {
  3876  							Result: statuspb.TestStatus_PASS,
  3877  						},
  3878  					},
  3879  				},
  3880  				{
  3881  					Column: &statepb.Column{Build: "2"},
  3882  					Cells: map[string]cell{
  3883  						"still-broken": {
  3884  							Result: statuspb.TestStatus_FAIL,
  3885  						},
  3886  						"fixed": {
  3887  							Result: statuspb.TestStatus_FAIL,
  3888  						},
  3889  					},
  3890  				},
  3891  				{
  3892  					Column: &statepb.Column{Build: "1"},
  3893  					Cells: map[string]cell{
  3894  						"still-broken": {
  3895  							Result: statuspb.TestStatus_FAIL,
  3896  						},
  3897  						"fixed": {
  3898  							Result: statuspb.TestStatus_FAIL,
  3899  						},
  3900  					},
  3901  				},
  3902  			},
  3903  			expected: &statepb.Grid{
  3904  				Columns: []*statepb.Column{
  3905  					{Build: "4"},
  3906  					{Build: "3"},
  3907  					{Build: "2"},
  3908  					{Build: "1"},
  3909  				},
  3910  				Rows: []*statepb.Row{
  3911  					setupRow(
  3912  						&statepb.Row{
  3913  							Name: "fixed",
  3914  							Id:   "fixed",
  3915  						},
  3916  						cell{Result: statuspb.TestStatus_PASS},
  3917  						cell{Result: statuspb.TestStatus_PASS},
  3918  						cell{Result: statuspb.TestStatus_FAIL},
  3919  						cell{Result: statuspb.TestStatus_FAIL},
  3920  					),
  3921  					setupRow(
  3922  						&statepb.Row{
  3923  							Name: "still-broken",
  3924  							Id:   "still-broken",
  3925  						},
  3926  						cell{Result: statuspb.TestStatus_PASS},
  3927  						cell{Result: statuspb.TestStatus_FAIL},
  3928  						cell{Result: statuspb.TestStatus_FAIL},
  3929  						cell{Result: statuspb.TestStatus_FAIL},
  3930  					),
  3931  				},
  3932  			},
  3933  		},
  3934  		{
  3935  			name: "issues",
  3936  			cols: []inflatedColumn{
  3937  				{
  3938  					Column: &statepb.Column{Build: "15"},
  3939  					Cells: map[string]cell{
  3940  						"row": {
  3941  							Result: statuspb.TestStatus_PASS,
  3942  							Issues: []string{
  3943  								"from-cell-15",
  3944  								"should-deduplicate-from-both",
  3945  								"should-deduplicate-from-row",
  3946  								"should-deduplicate-from-cell",
  3947  								"should-deduplicate-from-cell",
  3948  							},
  3949  						},
  3950  					},
  3951  				},
  3952  				{
  3953  					Column: &statepb.Column{Build: "10"},
  3954  					Cells: map[string]cell{
  3955  						"row": {
  3956  							Result: statuspb.TestStatus_PASS,
  3957  							Issues: []string{
  3958  								"from-cell-10",
  3959  								"should-deduplicate-from-row",
  3960  							},
  3961  						},
  3962  						"other": {
  3963  							Result: statuspb.TestStatus_PASS,
  3964  							Issues: []string{"fun"},
  3965  						},
  3966  						"sort": {
  3967  							Result: statuspb.TestStatus_PASS,
  3968  							Issues: []string{
  3969  								"3-is-second",
  3970  								"100-is-last",
  3971  								"2-is-first",
  3972  							},
  3973  						},
  3974  					},
  3975  				},
  3976  			},
  3977  			issues: map[string][]string{
  3978  				"row": {
  3979  					"from-argument",
  3980  					"should-deduplicate-from-arg",
  3981  					"should-deduplicate-from-arg",
  3982  					"should-deduplicate-from-both",
  3983  				},
  3984  			},
  3985  			expected: &statepb.Grid{
  3986  				Columns: []*statepb.Column{
  3987  					{Build: "15"},
  3988  					{Build: "10"},
  3989  				},
  3990  				Rows: []*statepb.Row{
  3991  					setupRow(
  3992  						&statepb.Row{
  3993  							Name:   "other",
  3994  							Id:     "other",
  3995  							Issues: []string{"fun"},
  3996  						},
  3997  						cell{Result: statuspb.TestStatus_NO_RESULT},
  3998  						cell{Result: statuspb.TestStatus_PASS},
  3999  					),
  4000  					setupRow(
  4001  						&statepb.Row{
  4002  							Name: "row",
  4003  							Id:   "row",
  4004  							Issues: []string{
  4005  								"should-deduplicate-from-row",
  4006  								"should-deduplicate-from-cell",
  4007  								"should-deduplicate-from-both",
  4008  								"should-deduplicate-from-arg",
  4009  								"from-cell-15",
  4010  								"from-cell-10",
  4011  								"from-argument",
  4012  							},
  4013  						},
  4014  						cell{Result: statuspb.TestStatus_PASS},
  4015  						cell{Result: statuspb.TestStatus_PASS},
  4016  					),
  4017  					setupRow(
  4018  						&statepb.Row{
  4019  							Name: "sort",
  4020  							Id:   "sort",
  4021  							Issues: []string{
  4022  								"100-is-last",
  4023  								"3-is-second",
  4024  								"2-is-first",
  4025  							},
  4026  						},
  4027  						cell{Result: statuspb.TestStatus_NO_RESULT},
  4028  						cell{Result: statuspb.TestStatus_PASS},
  4029  					),
  4030  				},
  4031  			},
  4032  		},
  4033  	}
  4034  
  4035  	for _, tc := range cases {
  4036  		t.Run(tc.name, func(t *testing.T) {
  4037  			actual := ConstructGrid(logrus.WithField("name", tc.name), tc.cols, tc.issues, tc.numFailuresToAlert, tc.numPassesToDisableAlert, true, "userProperty", tc.brokenThreshold, tc.columnHeader)
  4038  			alertRows(tc.expected.Columns, tc.expected.Rows, tc.numFailuresToAlert, tc.numPassesToDisableAlert, true, "userProperty", tc.columnHeader)
  4039  			for _, row := range tc.expected.Rows {
  4040  				sort.SliceStable(row.Metric, func(i, j int) bool {
  4041  					return sortorder.NaturalLess(row.Metric[i], row.Metric[j])
  4042  				})
  4043  				sort.SliceStable(row.Metrics, func(i, j int) bool {
  4044  					return sortorder.NaturalLess(row.Metrics[i].Name, row.Metrics[j].Name)
  4045  				})
  4046  			}
  4047  			if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" {
  4048  				t.Errorf("ConstructGrid() got unexpected diff (-want +got):\n%s", diff)
  4049  			}
  4050  		})
  4051  	}
  4052  }
  4053  
  4054  func TestAppendMetric(t *testing.T) {
  4055  	cases := []struct {
  4056  		name     string
  4057  		metric   *statepb.Metric
  4058  		idx      int32
  4059  		value    float64
  4060  		expected *statepb.Metric
  4061  	}{
  4062  		{
  4063  			name:   "basically works",
  4064  			metric: &statepb.Metric{},
  4065  			expected: &statepb.Metric{
  4066  				Indices: []int32{0, 1},
  4067  				Values:  []float64{0},
  4068  			},
  4069  		},
  4070  		{
  4071  			name:   "start metric at random column",
  4072  			metric: &statepb.Metric{},
  4073  			idx:    7,
  4074  			value:  11,
  4075  			expected: &statepb.Metric{
  4076  				Indices: []int32{7, 1},
  4077  				Values:  []float64{11},
  4078  			},
  4079  		},
  4080  		{
  4081  			name: "continue existing series",
  4082  			metric: &statepb.Metric{
  4083  				Indices: []int32{6, 2},
  4084  				Values:  []float64{6.1, 6.2},
  4085  			},
  4086  			idx:   8,
  4087  			value: 88,
  4088  			expected: &statepb.Metric{
  4089  				Indices: []int32{6, 3},
  4090  				Values:  []float64{6.1, 6.2, 88},
  4091  			},
  4092  		},
  4093  		{
  4094  			name: "start new series",
  4095  			metric: &statepb.Metric{
  4096  				Indices: []int32{3, 2},
  4097  				Values:  []float64{6.1, 6.2},
  4098  			},
  4099  			idx:   8,
  4100  			value: 88,
  4101  			expected: &statepb.Metric{
  4102  				Indices: []int32{3, 2, 8, 1},
  4103  				Values:  []float64{6.1, 6.2, 88},
  4104  			},
  4105  		},
  4106  	}
  4107  
  4108  	for _, tc := range cases {
  4109  		t.Run(tc.name, func(t *testing.T) {
  4110  			appendMetric(tc.metric, tc.idx, tc.value)
  4111  			if diff := cmp.Diff(tc.metric, tc.expected, protocmp.Transform()); diff != "" {
  4112  				t.Errorf("appendMetric() got unexpected diff (-got +want):\n%s", diff)
  4113  			}
  4114  		})
  4115  	}
  4116  }
  4117  
  4118  func TestAppendCell(t *testing.T) {
  4119  	cases := []struct {
  4120  		name  string
  4121  		row   *statepb.Row
  4122  		cell  cell
  4123  		start int
  4124  		count int
  4125  
  4126  		expected *statepb.Row
  4127  	}{
  4128  		{
  4129  			name: "basically works",
  4130  			row:  &statepb.Row{},
  4131  			expected: &statepb.Row{
  4132  				Results: []int32{0, 0},
  4133  			},
  4134  		},
  4135  		{
  4136  			name: "first result",
  4137  			row:  &statepb.Row{},
  4138  			cell: cell{
  4139  				Result: statuspb.TestStatus_PASS,
  4140  			},
  4141  			count: 1,
  4142  			expected: &statepb.Row{
  4143  				Results:      []int32{int32(statuspb.TestStatus_PASS), 1},
  4144  				CellIds:      []string{""},
  4145  				Messages:     []string{""},
  4146  				Icons:        []string{""},
  4147  				UserProperty: []string{""},
  4148  				Properties:   []*statepb.Property{{}},
  4149  			},
  4150  		},
  4151  		{
  4152  			name: "all fields filled",
  4153  			row:  &statepb.Row{},
  4154  			cell: cell{
  4155  				Result:  statuspb.TestStatus_PASS,
  4156  				CellID:  "cell-id",
  4157  				Message: "hi",
  4158  				Icon:    "there",
  4159  				Metrics: map[string]float64{
  4160  					"pi":     3.14,
  4161  					"golden": 1.618,
  4162  				},
  4163  				UserProperty: "hello",
  4164  				Properties: map[string]string{
  4165  					"workflow-id":   "run-1",
  4166  					"workflow-name": "//workflow-a",
  4167  				},
  4168  			},
  4169  			count: 1,
  4170  			expected: &statepb.Row{
  4171  				Results:  []int32{int32(statuspb.TestStatus_PASS), 1},
  4172  				CellIds:  []string{"cell-id"},
  4173  				Messages: []string{"hi"},
  4174  				Icons:    []string{"there"},
  4175  				Metric: []string{
  4176  					"golden",
  4177  					"pi",
  4178  				},
  4179  				Metrics: []*statepb.Metric{
  4180  					{
  4181  						Name:    "pi",
  4182  						Indices: []int32{0, 1},
  4183  						Values:  []float64{3.14},
  4184  					},
  4185  					{
  4186  						Name:    "golden",
  4187  						Indices: []int32{0, 1},
  4188  						Values:  []float64{1.618},
  4189  					},
  4190  				},
  4191  				UserProperty: []string{"hello"},
  4192  				Properties: []*statepb.Property{{
  4193  					Property: map[string]string{
  4194  						"workflow-id":   "run-1",
  4195  						"workflow-name": "//workflow-a",
  4196  					},
  4197  				}},
  4198  			},
  4199  		},
  4200  		{
  4201  			name: "append same result",
  4202  			row: &statepb.Row{
  4203  				Results: []int32{
  4204  					int32(statuspb.TestStatus_FLAKY), 3,
  4205  				},
  4206  				CellIds:      []string{"", "", ""},
  4207  				Messages:     []string{"", "", ""},
  4208  				Icons:        []string{"", "", ""},
  4209  				UserProperty: []string{"", "", ""},
  4210  				Properties:   []*statepb.Property{{}, {}, {}},
  4211  			},
  4212  			cell: cell{
  4213  				Result:       statuspb.TestStatus_FLAKY,
  4214  				Message:      "echo",
  4215  				CellID:       "again and",
  4216  				Icon:         "keeps going",
  4217  				UserProperty: "more more",
  4218  				Properties: map[string]string{
  4219  					"workflow-id":   "run-1",
  4220  					"workflow-name": "//workflow-a",
  4221  				},
  4222  			},
  4223  			count: 2,
  4224  			expected: &statepb.Row{
  4225  				Results:      []int32{int32(statuspb.TestStatus_FLAKY), 5},
  4226  				CellIds:      []string{"", "", "", "again and", "again and"},
  4227  				Messages:     []string{"", "", "", "echo", "echo"},
  4228  				Icons:        []string{"", "", "", "keeps going", "keeps going"},
  4229  				UserProperty: []string{"", "", "", "more more", "more more"},
  4230  				Properties: []*statepb.Property{
  4231  					{},
  4232  					{},
  4233  					{},
  4234  					{
  4235  						Property: map[string]string{
  4236  							"workflow-id":   "run-1",
  4237  							"workflow-name": "//workflow-a",
  4238  						},
  4239  					},
  4240  					{
  4241  						Property: map[string]string{
  4242  							"workflow-id":   "run-1",
  4243  							"workflow-name": "//workflow-a",
  4244  						},
  4245  					},
  4246  				},
  4247  			},
  4248  		},
  4249  		{
  4250  			name: "append different result",
  4251  			row: &statepb.Row{
  4252  				Results: []int32{
  4253  					int32(statuspb.TestStatus_FLAKY), 3,
  4254  				},
  4255  				CellIds:      []string{"", "", ""},
  4256  				Messages:     []string{"", "", ""},
  4257  				Icons:        []string{"", "", ""},
  4258  				UserProperty: []string{"", "", ""},
  4259  				Properties:   []*statepb.Property{{}, {}, {}},
  4260  			},
  4261  			cell: cell{
  4262  				Result: statuspb.TestStatus_PASS,
  4263  			},
  4264  			count: 2,
  4265  			expected: &statepb.Row{
  4266  				Results: []int32{
  4267  					int32(statuspb.TestStatus_FLAKY), 3,
  4268  					int32(statuspb.TestStatus_PASS), 2,
  4269  				},
  4270  				CellIds:      []string{"", "", "", "", ""},
  4271  				Messages:     []string{"", "", "", "", ""},
  4272  				Icons:        []string{"", "", "", "", ""},
  4273  				UserProperty: []string{"", "", "", "", ""},
  4274  				Properties:   []*statepb.Property{{}, {}, {}, {}, {}},
  4275  			},
  4276  		},
  4277  		{
  4278  			name: "append no Result (results, no cellIDs, messages or icons)",
  4279  			row: &statepb.Row{
  4280  				Results: []int32{
  4281  					int32(statuspb.TestStatus_FLAKY), 3,
  4282  				},
  4283  				CellIds:      []string{"", "", ""},
  4284  				Messages:     []string{"", "", ""},
  4285  				Icons:        []string{"", "", ""},
  4286  				UserProperty: []string{"", "", ""},
  4287  				Properties:   []*statepb.Property{{}, {}, {}},
  4288  			},
  4289  			cell: cell{
  4290  				Result: statuspb.TestStatus_NO_RESULT,
  4291  			},
  4292  			count: 2,
  4293  			expected: &statepb.Row{
  4294  				Results: []int32{
  4295  					int32(statuspb.TestStatus_FLAKY), 3,
  4296  					int32(statuspb.TestStatus_NO_RESULT), 2,
  4297  				},
  4298  				CellIds:      []string{"", "", ""},
  4299  				Messages:     []string{"", "", ""},
  4300  				Icons:        []string{"", "", ""},
  4301  				UserProperty: []string{"", "", ""},
  4302  				Properties:   []*statepb.Property{{}, {}, {}},
  4303  			},
  4304  		},
  4305  		{
  4306  			name: "add metric to series",
  4307  			row: &statepb.Row{
  4308  				Results:      []int32{int32(statuspb.TestStatus_PASS), 5},
  4309  				CellIds:      []string{"", "", "", "", "c"},
  4310  				Messages:     []string{"", "", "", "", "m"},
  4311  				Icons:        []string{"", "", "", "", "i"},
  4312  				UserProperty: []string{"", "", "", "", "up"},
  4313  				Properties:   []*statepb.Property{{}, {}, {}, {}, {}},
  4314  				Metric: []string{
  4315  					"continued-series",
  4316  					"new-series",
  4317  				},
  4318  				Metrics: []*statepb.Metric{
  4319  					{
  4320  						Name:    "continued-series",
  4321  						Indices: []int32{0, 5},
  4322  						Values:  []float64{0, 1, 2, 3, 4},
  4323  					},
  4324  					{
  4325  						Name:    "new-series",
  4326  						Indices: []int32{2, 2},
  4327  						Values:  []float64{2, 3},
  4328  					},
  4329  				},
  4330  			},
  4331  			cell: cell{
  4332  				Result: statuspb.TestStatus_PASS,
  4333  				Metrics: map[string]float64{
  4334  					"continued-series":  5.1,
  4335  					"new-series":        5.2,
  4336  					"additional-metric": 5.3,
  4337  				},
  4338  			},
  4339  			start: 5,
  4340  			count: 1,
  4341  			expected: &statepb.Row{
  4342  				Results:      []int32{int32(statuspb.TestStatus_PASS), 6},
  4343  				CellIds:      []string{"", "", "", "", "c", ""},
  4344  				Messages:     []string{"", "", "", "", "m", ""},
  4345  				Icons:        []string{"", "", "", "", "i", ""},
  4346  				UserProperty: []string{"", "", "", "", "up", ""},
  4347  				Properties:   []*statepb.Property{{}, {}, {}, {}, {}, {}},
  4348  				Metric: []string{
  4349  					"continued-series",
  4350  					"new-series",
  4351  					"additional-metric",
  4352  				},
  4353  				Metrics: []*statepb.Metric{
  4354  					{
  4355  						Name:    "continued-series",
  4356  						Indices: []int32{0, 6},
  4357  						Values:  []float64{0, 1, 2, 3, 4, 5.1},
  4358  					},
  4359  					{
  4360  						Name:    "new-series",
  4361  						Indices: []int32{2, 2, 5, 1},
  4362  						Values:  []float64{2, 3, 5.2},
  4363  					},
  4364  					{
  4365  						Name:    "additional-metric",
  4366  						Indices: []int32{5, 1},
  4367  						Values:  []float64{5.3},
  4368  					},
  4369  				},
  4370  			},
  4371  		},
  4372  		{
  4373  			name:  "add a bunch of initial blank columns (eg a deleted row)",
  4374  			row:   &statepb.Row{},
  4375  			cell:  emptyCell,
  4376  			count: 7,
  4377  			expected: &statepb.Row{
  4378  				Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 7},
  4379  			},
  4380  		},
  4381  		{
  4382  			name:  "issues",
  4383  			row:   &statepb.Row{},
  4384  			count: 395,
  4385  			cell: Cell{
  4386  				Issues: []string{"problematic", "state"},
  4387  			},
  4388  			expected: &statepb.Row{
  4389  				Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 395},
  4390  				Issues:  []string{"problematic", "state"},
  4391  			},
  4392  		},
  4393  		{
  4394  			name: "append to group delimiter",
  4395  			row: &statepb.Row{
  4396  				Name: "test1@TESTGRID@something",
  4397  				Results: []int32{
  4398  					int32(statuspb.TestStatus_PASS), 1,
  4399  				},
  4400  				Messages:     []string{""},
  4401  				Icons:        []string{""},
  4402  				UserProperty: []string{""},
  4403  			},
  4404  			cell: cell{
  4405  				Result: statuspb.TestStatus_PASS,
  4406  				CellID: "cell-id-1",
  4407  				Properties: map[string]string{
  4408  					"workflow-id":   "run-1",
  4409  					"workflow-name": "//workflow-a",
  4410  				},
  4411  			},
  4412  			count: 1,
  4413  			expected: &statepb.Row{
  4414  				Name: "test1@TESTGRID@something",
  4415  				Results: []int32{
  4416  					int32(statuspb.TestStatus_PASS), 2,
  4417  				},
  4418  				Messages:     []string{"", ""},
  4419  				Icons:        []string{"", ""},
  4420  				UserProperty: []string{"", ""},
  4421  			},
  4422  		},
  4423  	}
  4424  
  4425  	for _, tc := range cases {
  4426  		t.Run(tc.name, func(t *testing.T) {
  4427  			appendCell(tc.row, tc.cell, tc.start, tc.count)
  4428  			sort.SliceStable(tc.row.Metric, func(i, j int) bool {
  4429  				return tc.row.Metric[i] < tc.row.Metric[j]
  4430  			})
  4431  			sort.SliceStable(tc.row.Metrics, func(i, j int) bool {
  4432  				return tc.row.Metrics[i].Name < tc.row.Metrics[j].Name
  4433  			})
  4434  			sort.SliceStable(tc.expected.Metric, func(i, j int) bool {
  4435  				return tc.expected.Metric[i] < tc.expected.Metric[j]
  4436  			})
  4437  			sort.SliceStable(tc.expected.Metrics, func(i, j int) bool {
  4438  				return tc.expected.Metrics[i].Name < tc.expected.Metrics[j].Name
  4439  			})
  4440  			if diff := cmp.Diff(tc.row, tc.expected, protocmp.Transform()); diff != "" {
  4441  				t.Errorf("appendCell() got unexpected diff (-got +want):\n%s", diff)
  4442  			}
  4443  		})
  4444  	}
  4445  }
  4446  
  4447  // setupRow appends cells to the row.
  4448  //
  4449  // Auto-drops UserProperty if row.UserProperty == nil (set to empty to preserve).
  4450  func setupRow(row *statepb.Row, cells ...cell) *statepb.Row {
  4451  	dropUserPropety := row.UserProperty == nil
  4452  	for idx, c := range cells {
  4453  		appendCell(row, c, idx, 1)
  4454  	}
  4455  	if dropUserPropety {
  4456  		row.UserProperty = nil
  4457  	}
  4458  
  4459  	return row
  4460  }
  4461  
  4462  func TestAppendColumn(t *testing.T) {
  4463  	cases := []struct {
  4464  		name     string
  4465  		grid     *statepb.Grid
  4466  		col      inflatedColumn
  4467  		expected *statepb.Grid
  4468  	}{
  4469  		{
  4470  			name: "append first column",
  4471  			grid: &statepb.Grid{},
  4472  			col:  inflatedColumn{Column: &statepb.Column{Build: "10"}},
  4473  			expected: &statepb.Grid{
  4474  				Columns: []*statepb.Column{
  4475  					{Build: "10"},
  4476  				},
  4477  			},
  4478  		},
  4479  		{
  4480  			name: "append additional column",
  4481  			grid: &statepb.Grid{
  4482  				Columns: []*statepb.Column{
  4483  					{Build: "10"},
  4484  					{Build: "11"},
  4485  				},
  4486  			},
  4487  			col: inflatedColumn{Column: &statepb.Column{Build: "20"}},
  4488  			expected: &statepb.Grid{
  4489  				Columns: []*statepb.Column{
  4490  					{Build: "10"},
  4491  					{Build: "11"},
  4492  					{Build: "20"},
  4493  				},
  4494  			},
  4495  		},
  4496  		{
  4497  			name: "add rows to first column",
  4498  			grid: &statepb.Grid{},
  4499  			col: inflatedColumn{
  4500  				Column: &statepb.Column{Build: "10"},
  4501  				Cells: map[string]cell{
  4502  					"hello": {
  4503  						Result: statuspb.TestStatus_PASS,
  4504  						CellID: "yes",
  4505  						Metrics: map[string]float64{
  4506  							"answer": 42,
  4507  						},
  4508  					},
  4509  					"world": {
  4510  						Result:       statuspb.TestStatus_FAIL,
  4511  						Message:      "boom",
  4512  						Icon:         "X",
  4513  						UserProperty: "prop",
  4514  					},
  4515  				},
  4516  			},
  4517  			expected: &statepb.Grid{
  4518  				Columns: []*statepb.Column{
  4519  					{Build: "10"},
  4520  				},
  4521  				Rows: []*statepb.Row{
  4522  					setupRow(
  4523  						&statepb.Row{
  4524  							Name:         "hello",
  4525  							Id:           "hello",
  4526  							UserProperty: []string{},
  4527  						},
  4528  						cell{
  4529  							Result:  statuspb.TestStatus_PASS,
  4530  							CellID:  "yes",
  4531  							Metrics: map[string]float64{"answer": 42},
  4532  						}),
  4533  					setupRow(
  4534  						&statepb.Row{
  4535  							Name:         "world",
  4536  							Id:           "world",
  4537  							UserProperty: []string{},
  4538  						},
  4539  						cell{
  4540  							Result:       statuspb.TestStatus_FAIL,
  4541  							Message:      "boom",
  4542  							Icon:         "X",
  4543  							UserProperty: "prop",
  4544  						},
  4545  					),
  4546  				},
  4547  			},
  4548  		},
  4549  		{
  4550  			name: "add empty cells",
  4551  			grid: &statepb.Grid{
  4552  				Columns: []*statepb.Column{
  4553  					{Build: "10"},
  4554  					{Build: "11"},
  4555  					{Build: "12"},
  4556  				},
  4557  				Rows: []*statepb.Row{
  4558  					setupRow(
  4559  						&statepb.Row{
  4560  							Name:         "deleted",
  4561  							UserProperty: []string{},
  4562  						},
  4563  						cell{Result: statuspb.TestStatus_PASS},
  4564  						cell{Result: statuspb.TestStatus_PASS},
  4565  						cell{Result: statuspb.TestStatus_PASS},
  4566  					),
  4567  					setupRow(
  4568  						&statepb.Row{
  4569  							Name:         "always",
  4570  							UserProperty: []string{},
  4571  						},
  4572  						cell{Result: statuspb.TestStatus_PASS},
  4573  						cell{Result: statuspb.TestStatus_PASS},
  4574  						cell{Result: statuspb.TestStatus_PASS},
  4575  					),
  4576  				},
  4577  			},
  4578  			col: inflatedColumn{
  4579  				Column: &statepb.Column{Build: "20"},
  4580  				Cells: map[string]cell{
  4581  					"always": {Result: statuspb.TestStatus_PASS},
  4582  					"new":    {Result: statuspb.TestStatus_PASS},
  4583  				},
  4584  			},
  4585  			expected: &statepb.Grid{
  4586  				Columns: []*statepb.Column{
  4587  					{Build: "10"},
  4588  					{Build: "11"},
  4589  					{Build: "12"},
  4590  					{Build: "20"},
  4591  				},
  4592  				Rows: []*statepb.Row{
  4593  					setupRow(
  4594  						&statepb.Row{
  4595  							Name:         "deleted",
  4596  							UserProperty: []string{},
  4597  						},
  4598  						cell{Result: statuspb.TestStatus_PASS},
  4599  						cell{Result: statuspb.TestStatus_PASS},
  4600  						cell{Result: statuspb.TestStatus_PASS},
  4601  						emptyCell,
  4602  					),
  4603  					setupRow(
  4604  						&statepb.Row{
  4605  							Name:         "always",
  4606  							UserProperty: []string{},
  4607  						},
  4608  						cell{Result: statuspb.TestStatus_PASS},
  4609  						cell{Result: statuspb.TestStatus_PASS},
  4610  						cell{Result: statuspb.TestStatus_PASS},
  4611  						cell{Result: statuspb.TestStatus_PASS},
  4612  					),
  4613  					setupRow(
  4614  						&statepb.Row{
  4615  							Name:         "new",
  4616  							Id:           "new",
  4617  							UserProperty: []string{},
  4618  						},
  4619  						emptyCell,
  4620  						emptyCell,
  4621  						emptyCell,
  4622  						cell{Result: statuspb.TestStatus_PASS},
  4623  					),
  4624  				},
  4625  			},
  4626  		},
  4627  	}
  4628  
  4629  	for _, tc := range cases {
  4630  		t.Run(tc.name, func(t *testing.T) {
  4631  			rows := map[string]*statepb.Row{}
  4632  			for _, r := range tc.grid.Rows {
  4633  				rows[r.Name] = r
  4634  			}
  4635  			AppendColumn(tc.grid, rows, tc.col)
  4636  			sort.SliceStable(tc.grid.Rows, func(i, j int) bool {
  4637  				return tc.grid.Rows[i].Name < tc.grid.Rows[j].Name
  4638  			})
  4639  			sort.SliceStable(tc.expected.Rows, func(i, j int) bool {
  4640  				return tc.expected.Rows[i].Name < tc.expected.Rows[j].Name
  4641  			})
  4642  			if diff := cmp.Diff(&tc.expected, &tc.grid, protocmp.Transform()); diff != "" {
  4643  				t.Errorf("appendColumn() got unexpected diff (-want +got):\n%s", diff)
  4644  			}
  4645  		})
  4646  	}
  4647  }
  4648  
  4649  func TestDynamicEmails(t *testing.T) {
  4650  	columnWithEmails := statepb.Column{Build: "columnWithEmail", Started: 100 - float64(0), EmailAddresses: []string{"email1@", "email2@"}}
  4651  	anotherColumnWithEmails := statepb.Column{Build: "anotherColumnWithEmails", Started: 100 - float64(1), EmailAddresses: []string{"email3@", "email2@"}}
  4652  	customColumnHeaders := map[string]string{}
  4653  	cases := []struct {
  4654  		name     string
  4655  		row      *statepb.Row
  4656  		columns  []*statepb.Column
  4657  		expected *statepb.AlertInfo
  4658  	}{
  4659  		{
  4660  			name: "first column with dynamic emails",
  4661  			row: &statepb.Row{
  4662  				Results: []int32{
  4663  					int32(statuspb.TestStatus_FAIL), 1,
  4664  				},
  4665  				Messages: []string{""},
  4666  				CellIds:  []string{""},
  4667  			},
  4668  			columns:  []*statepb.Column{&columnWithEmails},
  4669  			expected: alertInfo(1, "", "", "", nil, &columnWithEmails, &columnWithEmails, nil, false, customColumnHeaders),
  4670  		},
  4671  		{
  4672  			name: "two column with dynamic emails, we get only the first one",
  4673  			row: &statepb.Row{
  4674  				Results: []int32{
  4675  					int32(statuspb.TestStatus_FAIL), 2,
  4676  				},
  4677  				Messages: []string{"", ""},
  4678  				CellIds:  []string{"", ""},
  4679  			},
  4680  			columns:  []*statepb.Column{&anotherColumnWithEmails, &columnWithEmails},
  4681  			expected: alertInfo(2, "", "", "", nil, &columnWithEmails, &anotherColumnWithEmails, nil, false, customColumnHeaders),
  4682  		},
  4683  		{
  4684  			name: "first column don't have results, second column emails on the alert",
  4685  			row: &statepb.Row{
  4686  				Results: []int32{
  4687  					int32(statuspb.TestStatus_NO_RESULT), 1,
  4688  					int32(statuspb.TestStatus_FAIL), 1,
  4689  				},
  4690  				Messages: []string{"", ""},
  4691  				CellIds:  []string{"", ""},
  4692  			},
  4693  			columns:  []*statepb.Column{&columnWithEmails, &anotherColumnWithEmails},
  4694  			expected: alertInfo(1, "", "", "", nil, &anotherColumnWithEmails, &anotherColumnWithEmails, nil, false, customColumnHeaders),
  4695  		},
  4696  	}
  4697  	for _, tc := range cases {
  4698  		defaultColumnHeaders := []*configpb.TestGroup_ColumnHeader{}
  4699  		actual := alertRow(tc.columns, tc.row, 1, 1, false, "", defaultColumnHeaders)
  4700  		if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" {
  4701  			t.Errorf("alertRow() not as expected (-want, +got): %s", diff)
  4702  		}
  4703  	}
  4704  }
  4705  
  4706  func TestAlertRow(t *testing.T) {
  4707  	var columns []*statepb.Column
  4708  	for i, id := range []string{"a", "b", "c", "d", "e", "f"} {
  4709  		columns = append(columns, &statepb.Column{
  4710  			Build:   id,
  4711  			Started: 100 - float64(i),
  4712  			Extra: []string{
  4713  				"world",
  4714  				"bar",
  4715  			},
  4716  		})
  4717  	}
  4718  	defaultColumnHeaders := []*configpb.TestGroup_ColumnHeader{
  4719  		{
  4720  			Property: "hello",
  4721  		},
  4722  		{
  4723  			Property: "foo",
  4724  		},
  4725  	}
  4726  	customColumnHeaders := map[string]string{
  4727  		"hello": "world",
  4728  		"foo":   "bar",
  4729  	}
  4730  	cases := []struct {
  4731  		name         string
  4732  		row          *statepb.Row
  4733  		failOpen     int
  4734  		passClose    int
  4735  		property     string
  4736  		columnHeader []*configpb.TestGroup_ColumnHeader
  4737  		expected     *statepb.AlertInfo
  4738  	}{
  4739  		{
  4740  			name: "never alert by default",
  4741  			row: &statepb.Row{
  4742  				Results: []int32{
  4743  					int32(statuspb.TestStatus_FAIL), 6,
  4744  				},
  4745  			},
  4746  		},
  4747  		{
  4748  			name: "passes do not alert",
  4749  			row: &statepb.Row{
  4750  				Results: []int32{
  4751  					int32(statuspb.TestStatus_PASS), 6,
  4752  				},
  4753  			},
  4754  			failOpen:  1,
  4755  			passClose: 3,
  4756  		},
  4757  		{
  4758  			name: "flakes do not alert",
  4759  			row: &statepb.Row{
  4760  				Results: []int32{
  4761  					int32(statuspb.TestStatus_FLAKY), 6,
  4762  				},
  4763  			},
  4764  			failOpen: 1,
  4765  		},
  4766  		{
  4767  			name: "intermittent failures do not alert",
  4768  			row: &statepb.Row{
  4769  				Results: []int32{
  4770  					int32(statuspb.TestStatus_FAIL), 2,
  4771  					int32(statuspb.TestStatus_PASS), 1,
  4772  					int32(statuspb.TestStatus_FAIL), 2,
  4773  				},
  4774  			},
  4775  			failOpen: 3,
  4776  		},
  4777  		{
  4778  			name:         "new failures alert",
  4779  			columnHeader: defaultColumnHeaders,
  4780  			row: &statepb.Row{
  4781  				Results: []int32{
  4782  					int32(statuspb.TestStatus_FAIL), 3,
  4783  					int32(statuspb.TestStatus_PASS), 3,
  4784  				},
  4785  				Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4786  				CellIds:  []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4787  			},
  4788  			failOpen: 3,
  4789  			expected: alertInfo(3, "no", "very wrong", "no", nil, columns[2], columns[0], columns[3], false, customColumnHeaders),
  4790  		},
  4791  		{
  4792  			name: "rows without cell IDs can alert",
  4793  			row: &statepb.Row{
  4794  				Results: []int32{
  4795  					int32(statuspb.TestStatus_FAIL), 3,
  4796  					int32(statuspb.TestStatus_PASS), 3,
  4797  				},
  4798  				Messages: []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4799  			},
  4800  			failOpen: 3,
  4801  			expected: alertInfo(3, "no", "", "", nil, columns[2], columns[0], columns[3], false, nil),
  4802  		},
  4803  		{
  4804  			name:         "too few passes do not close",
  4805  			columnHeader: defaultColumnHeaders,
  4806  			row: &statepb.Row{
  4807  				Results: []int32{
  4808  					int32(statuspb.TestStatus_PASS), 2,
  4809  					int32(statuspb.TestStatus_FAIL), 4,
  4810  				},
  4811  				Messages: []string{"nope", "no", "yay", "very wrong", "hi", "hello"},
  4812  				CellIds:  []string{"wrong", "no", "yep", "very wrong", "hi", "hello"},
  4813  			},
  4814  			failOpen:  1,
  4815  			passClose: 3,
  4816  			expected:  alertInfo(4, "yay", "hello", "yep", nil, columns[5], columns[2], nil, false, customColumnHeaders),
  4817  		},
  4818  		{
  4819  			name:         "flakes do not close",
  4820  			columnHeader: defaultColumnHeaders,
  4821  			row: &statepb.Row{
  4822  				Results: []int32{
  4823  					int32(statuspb.TestStatus_FLAKY), 2,
  4824  					int32(statuspb.TestStatus_FAIL), 4,
  4825  				},
  4826  				Messages: []string{"nope", "no", "yay", "very wrong", "hi", "hello"},
  4827  				CellIds:  []string{"wrong", "no", "yep", "very wrong", "hi", "hello"},
  4828  			},
  4829  			failOpen: 1,
  4830  			expected: alertInfo(4, "yay", "hello", "yep", nil, columns[5], columns[2], nil, false, customColumnHeaders),
  4831  		},
  4832  		{
  4833  			name: "failures after insufficient passes",
  4834  			row: &statepb.Row{
  4835  				Results: []int32{
  4836  					int32(statuspb.TestStatus_FAIL), 1,
  4837  					int32(statuspb.TestStatus_FLAKY), 1,
  4838  					int32(statuspb.TestStatus_FAIL), 1,
  4839  					int32(statuspb.TestStatus_PASS), 1,
  4840  					int32(statuspb.TestStatus_FAIL), 2,
  4841  				},
  4842  				Messages: []string{"newest-fail", "what", "carelessness", "okay", "alert-here", "misfortune"},
  4843  				CellIds:  []string{"f0", "flake", "f2", "p3", "f4", "f5"},
  4844  			},
  4845  			failOpen:  2,
  4846  			passClose: 2,
  4847  			expected:  alertInfo(4, "newest-fail", "f5", "f0", nil, columns[5], columns[0], nil, false, nil),
  4848  		},
  4849  		{
  4850  			name: "close alert",
  4851  			row: &statepb.Row{
  4852  				Results: []int32{
  4853  					int32(statuspb.TestStatus_PASS), 1,
  4854  					int32(statuspb.TestStatus_FAIL), 5,
  4855  				},
  4856  			},
  4857  			failOpen: 1,
  4858  		},
  4859  		{
  4860  			name: "track through empty results",
  4861  			row: &statepb.Row{
  4862  				Results: []int32{
  4863  					int32(statuspb.TestStatus_FAIL), 1,
  4864  					int32(statuspb.TestStatus_NO_RESULT), 1,
  4865  					int32(statuspb.TestStatus_FAIL), 4,
  4866  				},
  4867  				Messages: []string{"yay" /*no result */, "no", "buu", "wrong", "nono"},
  4868  				CellIds:  []string{"yay-cell" /*no result */, "no", "buzz", "wrong2", "nada"},
  4869  			},
  4870  			failOpen:  5,
  4871  			passClose: 2,
  4872  			expected:  alertInfo(5, "yay", "nada", "yay-cell", nil, columns[5], columns[0], nil, false, nil),
  4873  		},
  4874  		{
  4875  			name: "track passes through empty results",
  4876  			row: &statepb.Row{
  4877  				Results: []int32{
  4878  					int32(statuspb.TestStatus_PASS), 1,
  4879  					int32(statuspb.TestStatus_NO_RESULT), 1,
  4880  					int32(statuspb.TestStatus_PASS), 1,
  4881  					int32(statuspb.TestStatus_FAIL), 3,
  4882  				},
  4883  			},
  4884  			failOpen:  1,
  4885  			passClose: 2,
  4886  		},
  4887  		{
  4888  			name: "running cells advance compressed index",
  4889  			row: &statepb.Row{
  4890  				Results: []int32{
  4891  					int32(statuspb.TestStatus_RUNNING), 1,
  4892  					int32(statuspb.TestStatus_FAIL), 5,
  4893  				},
  4894  				Messages: []string{"running0", "fail1-expected", "fail2", "fail3", "fail4", "fail5"},
  4895  				CellIds:  []string{"wrong", "yep", "no2", "no3", "no4", "no5"},
  4896  			},
  4897  			failOpen: 1,
  4898  			expected: alertInfo(5, "fail1-expected", "no5", "yep", nil, columns[5], columns[1], nil, false, nil),
  4899  		},
  4900  		{
  4901  			name:         "complex",
  4902  			columnHeader: defaultColumnHeaders,
  4903  			row: &statepb.Row{
  4904  				Results: []int32{
  4905  					int32(statuspb.TestStatus_PASS), 1,
  4906  					int32(statuspb.TestStatus_FAIL), 1,
  4907  					int32(statuspb.TestStatus_PASS), 1,
  4908  					int32(statuspb.TestStatus_FAIL), 2,
  4909  					int32(statuspb.TestStatus_PASS), 1,
  4910  				},
  4911  				Messages: []string{"latest pass", "latest fail", "pass", "second fail", "first fail", "first pass"},
  4912  				CellIds:  []string{"no-p0", "no-f1", "no-p2", "no-f3", "yes-f4", "yes-p5"},
  4913  			},
  4914  			failOpen:  2,
  4915  			passClose: 2,
  4916  			expected:  alertInfo(3, "latest fail", "yes-f4", "no-f1", nil, columns[4], columns[1], columns[5], false, customColumnHeaders),
  4917  		},
  4918  		{
  4919  			name: "alert consecutive failures only",
  4920  			row: &statepb.Row{
  4921  				Results: []int32{
  4922  					int32(statuspb.TestStatus_PASS), 1,
  4923  					int32(statuspb.TestStatus_FAIL), 1,
  4924  					int32(statuspb.TestStatus_PASS), 1,
  4925  					int32(statuspb.TestStatus_FAIL), 1,
  4926  					int32(statuspb.TestStatus_PASS), 3,
  4927  				},
  4928  				Messages: []string{"latest pass", "latest fail", "pass", "second fail", "pass", "pass", "pass"},
  4929  				CellIds:  []string{"p0", "f1", "p2", "f3", "p4", "p5", "p6"},
  4930  			},
  4931  			failOpen:  2,
  4932  			passClose: 3,
  4933  		},
  4934  		{
  4935  			name: "properties",
  4936  			row: &statepb.Row{
  4937  				Results: []int32{
  4938  					int32(statuspb.TestStatus_FAIL), 3,
  4939  					int32(statuspb.TestStatus_PASS), 3,
  4940  				},
  4941  				Messages:     []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4942  				CellIds:      []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4943  				UserProperty: []string{"prop0", "prop1", "prop2", "prop3", "prop4", "prop5"},
  4944  			},
  4945  			failOpen: 3,
  4946  			property: "some-prop",
  4947  			expected: alertInfo(3, "no", "very wrong", "no", map[string]string{"some-prop": "prop0"}, columns[2], columns[0], columns[3], false, nil),
  4948  		},
  4949  		{
  4950  			name:         "properties after passes",
  4951  			columnHeader: defaultColumnHeaders,
  4952  			row: &statepb.Row{
  4953  				Results: []int32{
  4954  					int32(statuspb.TestStatus_PASS), 2,
  4955  					int32(statuspb.TestStatus_FAIL), 3,
  4956  					int32(statuspb.TestStatus_PASS), 1,
  4957  				},
  4958  				Messages:     []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4959  				CellIds:      []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4960  				UserProperty: []string{"prop0", "prop1", "prop2", "prop3", "prop4", "prop5"},
  4961  			},
  4962  			failOpen:  3,
  4963  			passClose: 3,
  4964  			property:  "some-prop",
  4965  			expected:  alertInfo(3, "very wrong", "hi", "very wrong", map[string]string{"some-prop": "prop2"}, columns[4], columns[2], columns[5], false, customColumnHeaders),
  4966  		},
  4967  		{
  4968  			name: "empty properties",
  4969  			row: &statepb.Row{
  4970  				Results: []int32{
  4971  					int32(statuspb.TestStatus_FAIL), 3,
  4972  					int32(statuspb.TestStatus_PASS), 3,
  4973  				},
  4974  				Messages:     []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4975  				CellIds:      []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4976  				UserProperty: []string{},
  4977  			},
  4978  			failOpen: 3,
  4979  			property: "some-prop",
  4980  			expected: alertInfo(3, "no", "very wrong", "no", nil, columns[2], columns[0], columns[3], false, nil),
  4981  		},
  4982  		{
  4983  			name: "insufficient properties",
  4984  			row: &statepb.Row{
  4985  				Results: []int32{
  4986  					int32(statuspb.TestStatus_PASS), 2,
  4987  					int32(statuspb.TestStatus_FAIL), 4,
  4988  				},
  4989  				Messages:     []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4990  				CellIds:      []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  4991  				UserProperty: []string{"prop0"},
  4992  			},
  4993  			failOpen:  3,
  4994  			passClose: 3,
  4995  			property:  "some-prop",
  4996  			expected:  alertInfo(4, "very wrong", "hello", "very wrong", nil, columns[5], columns[2], nil, false, nil),
  4997  		},
  4998  		{
  4999  			name: "insufficient column header values",
  5000  			columnHeader: append(
  5001  				defaultColumnHeaders,
  5002  				&configpb.TestGroup_ColumnHeader{
  5003  					Property: "extra-key",
  5004  				},
  5005  			),
  5006  			row: &statepb.Row{
  5007  				Results: []int32{
  5008  					int32(statuspb.TestStatus_PASS), 2,
  5009  					int32(statuspb.TestStatus_FAIL), 4,
  5010  				},
  5011  				Messages:     []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  5012  				CellIds:      []string{"no", "no again", "very wrong", "yes", "hi", "hello"},
  5013  				UserProperty: []string{"prop0"},
  5014  			},
  5015  			failOpen:  3,
  5016  			passClose: 3,
  5017  			property:  "some-prop",
  5018  			expected:  alertInfo(4, "very wrong", "hello", "very wrong", nil, columns[5], columns[2], nil, false, customColumnHeaders),
  5019  		},
  5020  	}
  5021  
  5022  	for _, tc := range cases {
  5023  		t.Run(tc.name, func(t *testing.T) {
  5024  			actual := alertRow(columns, tc.row, tc.failOpen, tc.passClose, false, tc.property, tc.columnHeader)
  5025  			if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" {
  5026  				t.Errorf("alertRow() not as expected (-want, +got): %s", diff)
  5027  			}
  5028  		})
  5029  	}
  5030  }
  5031  
  5032  func TestBuildID(t *testing.T) {
  5033  	cases := []struct {
  5034  		name      string
  5035  		build     string
  5036  		extra     string
  5037  		useCommit bool
  5038  		expected  string
  5039  	}{
  5040  		{
  5041  			name: "return empty by default",
  5042  		},
  5043  		{
  5044  			name:      "use header as commit",
  5045  			build:     "wrong",
  5046  			extra:     "right",
  5047  			useCommit: true,
  5048  			expected:  "right",
  5049  		},
  5050  		{
  5051  			name:     "use build otherwise",
  5052  			build:    "right",
  5053  			extra:    "wrong",
  5054  			expected: "right",
  5055  		},
  5056  	}
  5057  
  5058  	for _, tc := range cases {
  5059  		t.Run(tc.name, func(t *testing.T) {
  5060  			col := statepb.Column{
  5061  				Build: tc.build,
  5062  			}
  5063  			if tc.extra != "" {
  5064  				col.Extra = append(col.Extra, tc.extra)
  5065  			}
  5066  			if actual := buildID(&col, tc.useCommit); actual != tc.expected {
  5067  				t.Errorf("%q != expected %q", actual, tc.expected)
  5068  			}
  5069  		})
  5070  	}
  5071  }
  5072  
  5073  func TestStamp(t *testing.T) {
  5074  	cases := []struct {
  5075  		name     string
  5076  		col      *statepb.Column
  5077  		expected *timestamp.Timestamp
  5078  	}{
  5079  		{
  5080  			name: "0 returns nil",
  5081  		},
  5082  		{
  5083  			name: "no nanos",
  5084  			col: &statepb.Column{
  5085  				Started: 2000,
  5086  			},
  5087  			expected: &timestamp.Timestamp{
  5088  				Seconds: 2,
  5089  				Nanos:   0,
  5090  			},
  5091  		},
  5092  		{
  5093  			name: "milli to nano",
  5094  			col: &statepb.Column{
  5095  				Started: 1234,
  5096  			},
  5097  			expected: &timestamp.Timestamp{
  5098  				Seconds: 1,
  5099  				Nanos:   234000000,
  5100  			},
  5101  		},
  5102  		{
  5103  			name: "double to nanos",
  5104  			col: &statepb.Column{
  5105  				Started: 1.1,
  5106  			},
  5107  			expected: &timestamp.Timestamp{
  5108  				Seconds: 0,
  5109  				Nanos:   1100000,
  5110  			},
  5111  		},
  5112  	}
  5113  
  5114  	for _, tc := range cases {
  5115  		t.Run(tc.name, func(t *testing.T) {
  5116  			if actual := stamp(tc.col); !reflect.DeepEqual(actual, tc.expected) {
  5117  				t.Errorf("stamp %s != expected stamp %s", actual, tc.expected)
  5118  			}
  5119  		})
  5120  	}
  5121  }
  5122  
  5123  func TestDropEmptyRows(t *testing.T) {
  5124  	cases := []struct {
  5125  		name     string
  5126  		results  map[string][]int32
  5127  		expected map[string][]int32
  5128  	}{
  5129  		{
  5130  			name:     "basically works",
  5131  			expected: map[string][]int32{},
  5132  		},
  5133  		{
  5134  			name: "keep everything",
  5135  			results: map[string][]int32{
  5136  				"pass":    {int32(statuspb.TestStatus_PASS), 1},
  5137  				"fail":    {int32(statuspb.TestStatus_FAIL), 2},
  5138  				"running": {int32(statuspb.TestStatus_RUNNING), 3},
  5139  			},
  5140  			expected: map[string][]int32{
  5141  				"pass":    {int32(statuspb.TestStatus_PASS), 1},
  5142  				"fail":    {int32(statuspb.TestStatus_FAIL), 2},
  5143  				"running": {int32(statuspb.TestStatus_RUNNING), 3},
  5144  			},
  5145  		},
  5146  		{
  5147  			name: "keep mixture",
  5148  			results: map[string][]int32{
  5149  				"was empty": {
  5150  					int32(statuspb.TestStatus_PASS), 1,
  5151  					int32(statuspb.TestStatus_NO_RESULT), 1,
  5152  				},
  5153  				"now empty": {
  5154  					int32(statuspb.TestStatus_NO_RESULT), 2,
  5155  					int32(statuspb.TestStatus_FAIL), 2,
  5156  				},
  5157  			},
  5158  			expected: map[string][]int32{
  5159  				"was empty": {
  5160  					int32(statuspb.TestStatus_PASS), 1,
  5161  					int32(statuspb.TestStatus_NO_RESULT), 1,
  5162  				},
  5163  				"now empty": {
  5164  					int32(statuspb.TestStatus_NO_RESULT), 2,
  5165  					int32(statuspb.TestStatus_FAIL), 2,
  5166  				},
  5167  			},
  5168  		},
  5169  		{
  5170  			name: "drop everything",
  5171  			results: map[string][]int32{
  5172  				"drop": {int32(statuspb.TestStatus_NO_RESULT), 1},
  5173  				"gone": {int32(statuspb.TestStatus_NO_RESULT), 10},
  5174  				"poof": {int32(statuspb.TestStatus_NO_RESULT), 100},
  5175  			},
  5176  			expected: map[string][]int32{},
  5177  		},
  5178  	}
  5179  
  5180  	for _, tc := range cases {
  5181  		t.Run(tc.name, func(t *testing.T) {
  5182  			var grid statepb.Grid
  5183  			rows := make(map[string]*statepb.Row, len(tc.results))
  5184  			for name, res := range tc.results {
  5185  				r := &statepb.Row{Name: name}
  5186  				r.Results = res
  5187  				grid.Rows = append(grid.Rows, r)
  5188  				rows[name] = r
  5189  			}
  5190  			dropEmptyRows(logrus.WithField("name", tc.name), &grid, rows)
  5191  			actualRowMap := make(map[string]*statepb.Row, len(grid.Rows))
  5192  			for _, r := range grid.Rows {
  5193  				actualRowMap[r.Name] = r
  5194  			}
  5195  
  5196  			if diff := cmp.Diff(rows, actualRowMap, protocmp.Transform()); diff != "" {
  5197  				t.Fatalf("dropEmptyRows() unmatched row maps (-grid, +map):\n%s", diff)
  5198  			}
  5199  
  5200  			actual := make(map[string][]int32, len(rows))
  5201  			for name, row := range rows {
  5202  				actual[name] = row.Results
  5203  			}
  5204  
  5205  			if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" {
  5206  				t.Errorf("dropEmptyRows() got unexpected diff (-have, +want):\n%s", diff)
  5207  			}
  5208  		})
  5209  	}
  5210  }
  5211  
  5212  func TestTruncate(t *testing.T) {
  5213  	cases := []struct {
  5214  		name string
  5215  		msg  string
  5216  		max  int
  5217  		want string
  5218  	}{
  5219  		{
  5220  			name: "empty",
  5221  			msg:  "",
  5222  			max:  20,
  5223  			want: "",
  5224  		},
  5225  		{
  5226  			name: "short",
  5227  			msg:  "short message",
  5228  			max:  20,
  5229  			want: "short message",
  5230  		},
  5231  		{
  5232  			name: "long",
  5233  			msg:  "i'm too long of a message, oh no what will i do",
  5234  			max:  20,
  5235  			want: "i'm too lo... will i do",
  5236  		},
  5237  		{
  5238  			name: "long runes",
  5239  			msg:  "庭には二羽鶏がいる。", // In the yard two chickens are there.
  5240  			max:  20,
  5241  			want: "庭には...いる。",
  5242  		},
  5243  		{
  5244  			name: "short runes",
  5245  			msg:  "鶏がいる。", // Two chickens are there.
  5246  			max:  20,
  5247  			want: "鶏がいる。",
  5248  		},
  5249  		{
  5250  			name: "small max",
  5251  			msg:  "short message",
  5252  			max:  2,
  5253  			want: "s...e",
  5254  		},
  5255  		{
  5256  			name: "odd max",
  5257  			msg:  "short message",
  5258  			max:  5,
  5259  			want: "sh...ge",
  5260  		},
  5261  		{
  5262  			name: "max 1",
  5263  			msg:  "short message",
  5264  			max:  1,
  5265  			want: "...",
  5266  		},
  5267  		{
  5268  			name: "max 0",
  5269  			msg:  "short message",
  5270  			max:  0,
  5271  			want: "short message",
  5272  		},
  5273  	}
  5274  
  5275  	for _, tc := range cases {
  5276  		t.Run(tc.name, func(t *testing.T) {
  5277  			if got := truncate(tc.msg, tc.max); got != tc.want {
  5278  				t.Errorf("truncate(%q, %d) got %q, want %q", tc.msg, tc.max, got, tc.want)
  5279  			}
  5280  		})
  5281  	}
  5282  }
  5283  
  5284  func TestHotlistIDs(t *testing.T) {
  5285  	cases := []struct {
  5286  		name       string
  5287  		hotlistIDs string
  5288  		want       []string
  5289  	}{
  5290  		{
  5291  			name:       "none",
  5292  			hotlistIDs: "",
  5293  			want:       nil,
  5294  		},
  5295  		{
  5296  			name:       "empty",
  5297  			hotlistIDs: ",,",
  5298  			want:       nil,
  5299  		},
  5300  		{
  5301  			name:       "one",
  5302  			hotlistIDs: "123",
  5303  			want:       []string{"123"},
  5304  		},
  5305  		{
  5306  			name:       "many",
  5307  			hotlistIDs: "123,456,789",
  5308  			want:       []string{"123", "456", "789"},
  5309  		},
  5310  		{
  5311  			name:       "spaces",
  5312  			hotlistIDs: "123 , 456, 789 ",
  5313  			want:       []string{"123", "456", "789"},
  5314  		},
  5315  		{
  5316  			name:       "many empty",
  5317  			hotlistIDs: "123,,456,",
  5318  			want:       []string{"123", "456"},
  5319  		},
  5320  		{
  5321  			name:       "complex",
  5322  			hotlistIDs: " 123,456,,, 789 ,",
  5323  			want:       []string{"123", "456", "789"},
  5324  		},
  5325  	}
  5326  
  5327  	for _, tc := range cases {
  5328  		t.Run(tc.name, func(t *testing.T) {
  5329  			col := &statepb.Column{
  5330  				HotlistIds: tc.hotlistIDs,
  5331  			}
  5332  			got := hotlistIDs(col)
  5333  			if diff := cmp.Diff(tc.want, got); diff != "" {
  5334  				t.Errorf("hotlistIDs(%v) differed (-want, +got): %s", col, diff)
  5335  			}
  5336  		})
  5337  	}
  5338  }
  5339  
  5340  func TestTruncateLastColumn(t *testing.T) {
  5341  	cases := []struct {
  5342  		name   string
  5343  		grid   []InflatedColumn
  5344  		expect []InflatedColumn
  5345  	}{
  5346  		{
  5347  			name:   "empty grid",
  5348  			grid:   []InflatedColumn{},
  5349  			expect: []inflatedColumn{},
  5350  		},
  5351  		{
  5352  			name:   "nil grid",
  5353  			grid:   nil,
  5354  			expect: nil,
  5355  		},
  5356  		{
  5357  			name: "grid",
  5358  			grid: []InflatedColumn{
  5359  				{
  5360  					Cells: map[string]Cell{
  5361  						"row_1": {
  5362  							ID:     "row_1",
  5363  							Result: statuspb.TestStatus_PASS,
  5364  						},
  5365  						"row_2": {
  5366  							ID:     "row_2",
  5367  							Result: statuspb.TestStatus_FAIL,
  5368  						},
  5369  					},
  5370  				},
  5371  				{
  5372  					Cells: map[string]Cell{
  5373  						"row_1": {
  5374  							ID:     "row_1",
  5375  							Result: statuspb.TestStatus_PASS,
  5376  						},
  5377  						"row_2": {
  5378  							ID:     "row_2",
  5379  							Result: statuspb.TestStatus_PASS,
  5380  						},
  5381  					},
  5382  				},
  5383  			},
  5384  			expect: []InflatedColumn{
  5385  				{
  5386  					Cells: map[string]Cell{
  5387  						"row_1": {
  5388  							ID:     "row_1",
  5389  							Result: statuspb.TestStatus_PASS,
  5390  						},
  5391  						"row_2": {
  5392  							ID:     "row_2",
  5393  							Result: statuspb.TestStatus_FAIL,
  5394  						},
  5395  					},
  5396  				},
  5397  				{
  5398  					Cells: map[string]Cell{
  5399  						"row_1": {
  5400  							ID:      "row_1",
  5401  							Result:  statuspb.TestStatus_UNKNOWN,
  5402  							Icon:    "...",
  5403  							Message: "100 candy grid exceeds maximum size of 10 candys",
  5404  						},
  5405  						"row_2": {
  5406  							ID:      "row_2",
  5407  							Result:  statuspb.TestStatus_UNKNOWN,
  5408  							Icon:    "...",
  5409  							Message: "100 candy grid exceeds maximum size of 10 candys",
  5410  						},
  5411  					},
  5412  				},
  5413  			},
  5414  		},
  5415  	}
  5416  
  5417  	for _, tc := range cases {
  5418  		t.Run(tc.name, func(t *testing.T) {
  5419  			actual := tc.grid
  5420  			truncateLastColumn(actual, 100, 10, "candy")
  5421  
  5422  			if diff := cmp.Diff(actual, tc.expect); diff != "" {
  5423  				t.Error("mismatch (+got, -want)")
  5424  				t.Log(diff)
  5425  			}
  5426  		})
  5427  	}
  5428  }