github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/summarizer/summary_test.go (about)

     1  /*
     2  Copyright 2019 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 summarizer
    18  
    19  import (
    20  	"bytes"
    21  	"compress/zlib"
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"sort"
    28  	"testing"
    29  	"time"
    30  
    31  	"bitbucket.org/creachadair/stringset"
    32  	"cloud.google.com/go/storage"
    33  	"github.com/golang/protobuf/proto"
    34  	"github.com/golang/protobuf/ptypes/timestamp"
    35  	"github.com/google/go-cmp/cmp"
    36  	"github.com/google/go-cmp/cmp/cmpopts"
    37  	"github.com/sirupsen/logrus"
    38  	"google.golang.org/protobuf/testing/protocmp"
    39  
    40  	"github.com/GoogleCloudPlatform/testgrid/internal/result"
    41  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    42  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    43  	summarypb "github.com/GoogleCloudPlatform/testgrid/pb/summary"
    44  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    45  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    46  	"github.com/GoogleCloudPlatform/testgrid/util/gcs/fake"
    47  )
    48  
    49  type fakeGroup struct {
    50  	group *configpb.TestGroup
    51  	grid  *statepb.Grid
    52  	mod   time.Time
    53  	gen   int64
    54  	err   error
    55  }
    56  
    57  func TestUpdate(t *testing.T) {
    58  	cases := []struct {
    59  		name string
    60  	}{
    61  		{},
    62  	}
    63  
    64  	for _, tc := range cases {
    65  		t.Run(tc.name, func(t *testing.T) {
    66  			// TODO(fejta): implement
    67  		})
    68  	}
    69  }
    70  
    71  func TestUpdateDashboard(t *testing.T) {
    72  	cases := []struct {
    73  		name     string
    74  		dash     *configpb.Dashboard
    75  		groups   map[string]fakeGroup
    76  		tabMode  bool
    77  		expected *summarypb.DashboardSummary
    78  		err      bool
    79  	}{
    80  		{
    81  			name: "basically works",
    82  			dash: &configpb.Dashboard{
    83  				Name: "stale-dashboard",
    84  				DashboardTab: []*configpb.DashboardTab{
    85  					{
    86  						Name:          "stale-tab",
    87  						TestGroupName: "foo-group",
    88  						AlertOptions: &configpb.DashboardTabAlertOptions{
    89  							AlertStaleResultsHours: 1,
    90  						},
    91  					},
    92  				},
    93  			},
    94  			groups: map[string]fakeGroup{
    95  				"foo-group": {
    96  					group: &configpb.TestGroup{},
    97  					grid:  &statepb.Grid{},
    98  					mod:   time.Unix(1000, 0),
    99  				},
   100  			},
   101  			expected: &summarypb.DashboardSummary{
   102  				TabSummaries: []*summarypb.DashboardTabSummary{
   103  					{
   104  						DashboardName:       "stale-dashboard",
   105  						DashboardTabName:    "stale-tab",
   106  						LastUpdateTimestamp: 1000,
   107  						Alert:               noRuns,
   108  						OverallStatus:       summarypb.DashboardTabSummary_STALE,
   109  						Status:              noRuns,
   110  						LatestGreen:         noGreens,
   111  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   112  					},
   113  				},
   114  			},
   115  		},
   116  		{
   117  			name: "still update working tabs when some tabs fail",
   118  			dash: &configpb.Dashboard{
   119  				Name: "a-dashboard",
   120  				DashboardTab: []*configpb.DashboardTab{
   121  					{
   122  						Name:          "working",
   123  						TestGroupName: "working-group",
   124  						AlertOptions: &configpb.DashboardTabAlertOptions{
   125  							AlertStaleResultsHours: 1,
   126  						},
   127  					},
   128  					{
   129  						Name:          "missing-tab",
   130  						TestGroupName: "group-not-present",
   131  						AlertOptions: &configpb.DashboardTabAlertOptions{
   132  							AlertStaleResultsHours: 1,
   133  						},
   134  					},
   135  					{
   136  						Name:          "error-tab",
   137  						TestGroupName: "has-errors",
   138  						AlertOptions: &configpb.DashboardTabAlertOptions{
   139  							AlertStaleResultsHours: 1,
   140  						},
   141  					},
   142  					{
   143  						Name:          "still-working",
   144  						TestGroupName: "working-group",
   145  						AlertOptions: &configpb.DashboardTabAlertOptions{
   146  							AlertStaleResultsHours: 1,
   147  						},
   148  					},
   149  				},
   150  			},
   151  			groups: map[string]fakeGroup{
   152  				"working-group": {
   153  					mod:   time.Unix(1000, 0),
   154  					group: &configpb.TestGroup{},
   155  					grid:  &statepb.Grid{},
   156  				},
   157  				"has-errors": {
   158  					err:   errors.New("tragedy"),
   159  					group: &configpb.TestGroup{},
   160  					grid:  &statepb.Grid{},
   161  				},
   162  			},
   163  			expected: &summarypb.DashboardSummary{
   164  				TabSummaries: []*summarypb.DashboardTabSummary{
   165  					{
   166  						DashboardName:       "a-dashboard",
   167  						DashboardTabName:    "working",
   168  						LastUpdateTimestamp: 1000,
   169  						Alert:               noRuns,
   170  						Status:              noRuns,
   171  						OverallStatus:       summarypb.DashboardTabSummary_STALE,
   172  						LatestGreen:         noGreens,
   173  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   174  					},
   175  					tabStatus("a-dashboard", "missing-tab", `Test group does not exist: "group-not-present"`),
   176  					tabStatus("a-dashboard", "error-tab", fmt.Sprintf("Error attempting to summarize tab: load has-errors: open: tragedy")),
   177  					{
   178  						DashboardName:       "a-dashboard",
   179  						DashboardTabName:    "still-working",
   180  						LastUpdateTimestamp: 1000,
   181  						Alert:               noRuns,
   182  						Status:              noRuns,
   183  						OverallStatus:       summarypb.DashboardTabSummary_STALE,
   184  						LatestGreen:         noGreens,
   185  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   186  					},
   187  				},
   188  			},
   189  			err: true,
   190  		},
   191  		{
   192  			name: "bug url",
   193  			dash: &configpb.Dashboard{
   194  				Name: "a-dashboard",
   195  				DashboardTab: []*configpb.DashboardTab{
   196  					{
   197  						Name:          "none",
   198  						TestGroupName: "a-group",
   199  					},
   200  					{
   201  						Name:            "empty",
   202  						TestGroupName:   "a-group",
   203  						OpenBugTemplate: &configpb.LinkTemplate{},
   204  					},
   205  					{
   206  						Name:          "url",
   207  						TestGroupName: "a-group",
   208  						OpenBugTemplate: &configpb.LinkTemplate{
   209  							Url: "http://some-bugs/",
   210  						},
   211  					},
   212  					{
   213  						Name:          "url-options-empty",
   214  						TestGroupName: "a-group",
   215  						OpenBugTemplate: &configpb.LinkTemplate{
   216  							Url:     "http://more-bugs/",
   217  							Options: []*configpb.LinkOptionsTemplate{},
   218  						},
   219  					},
   220  					{
   221  						Name:          "url-options",
   222  						TestGroupName: "a-group",
   223  						OpenBugTemplate: &configpb.LinkTemplate{
   224  							Url: "http://ooh-bugs/",
   225  							Options: []*configpb.LinkOptionsTemplate{
   226  								{
   227  									Key:   "id",
   228  									Value: "warble",
   229  								},
   230  								{
   231  									Key:   "name",
   232  									Value: "garble",
   233  								},
   234  							},
   235  						},
   236  					},
   237  				},
   238  			},
   239  			groups: map[string]fakeGroup{
   240  				"a-group": {
   241  					mod:   time.Unix(1000, 0),
   242  					group: &configpb.TestGroup{},
   243  					grid:  &statepb.Grid{},
   244  				},
   245  			},
   246  			expected: &summarypb.DashboardSummary{
   247  				TabSummaries: []*summarypb.DashboardTabSummary{
   248  					{
   249  						DashboardName:       "a-dashboard",
   250  						DashboardTabName:    "none",
   251  						LastUpdateTimestamp: 1000,
   252  						Status:              noRuns,
   253  						OverallStatus:       summarypb.DashboardTabSummary_UNKNOWN,
   254  						LatestGreen:         noGreens,
   255  						BugUrl:              "",
   256  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   257  					},
   258  					{
   259  						DashboardName:       "a-dashboard",
   260  						DashboardTabName:    "empty",
   261  						LastUpdateTimestamp: 1000,
   262  						Status:              noRuns,
   263  						OverallStatus:       summarypb.DashboardTabSummary_UNKNOWN,
   264  						LatestGreen:         noGreens,
   265  						BugUrl:              "",
   266  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   267  					},
   268  					{
   269  						DashboardName:       "a-dashboard",
   270  						DashboardTabName:    "url",
   271  						LastUpdateTimestamp: 1000,
   272  						Status:              noRuns,
   273  						OverallStatus:       summarypb.DashboardTabSummary_UNKNOWN,
   274  						LatestGreen:         noGreens,
   275  						BugUrl:              "http://some-bugs/",
   276  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   277  					},
   278  					{
   279  						DashboardName:       "a-dashboard",
   280  						DashboardTabName:    "url-options-empty",
   281  						LastUpdateTimestamp: 1000,
   282  						Status:              noRuns,
   283  						OverallStatus:       summarypb.DashboardTabSummary_UNKNOWN,
   284  						LatestGreen:         noGreens,
   285  						BugUrl:              "http://more-bugs/",
   286  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   287  					},
   288  					{
   289  						DashboardName:       "a-dashboard",
   290  						DashboardTabName:    "url-options",
   291  						LastUpdateTimestamp: 1000,
   292  						Status:              noRuns,
   293  						OverallStatus:       summarypb.DashboardTabSummary_UNKNOWN,
   294  						LatestGreen:         noGreens,
   295  						BugUrl:              "http://ooh-bugs/",
   296  						SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   297  					},
   298  				},
   299  			},
   300  		},
   301  	}
   302  
   303  	for _, tc := range cases {
   304  		tabUpdater := tabUpdatePool(context.Background(), logrus.WithField("name", "pool"), 5, FeatureFlags{false, false, false})
   305  		t.Run(tc.name, func(t *testing.T) {
   306  			finder := func(dash string, tab *configpb.DashboardTab) (*gcs.Path, *configpb.TestGroup, gridReader, error) {
   307  				name := tab.TestGroupName
   308  				if name == "inject-error" {
   309  					return nil, nil, nil, errors.New("injected find group error")
   310  				}
   311  				fake, ok := tc.groups[name]
   312  				if !ok {
   313  					return nil, nil, nil, nil
   314  				}
   315  				var path *gcs.Path
   316  				var err error
   317  
   318  				path, err = gcs.NewPath(fmt.Sprintf("gs://bucket/grid/%s/%s", dash, name))
   319  				if err != nil {
   320  					t.Helper()
   321  					t.Fatalf("Failed to create path: %v", err)
   322  				}
   323  				reader := func(_ context.Context) (io.ReadCloser, time.Time, int64, error) {
   324  					return ioutil.NopCloser(bytes.NewBuffer(compress(gridBuf(fake.grid)))), fake.mod, fake.gen, fake.err
   325  				}
   326  				return path, fake.group, reader, nil
   327  			}
   328  			var actual summarypb.DashboardSummary
   329  			client := fake.Stater{}
   330  			for name, group := range tc.groups {
   331  				path, err := gcs.NewPath(fmt.Sprintf("gs://bucket/grid/%s/%s", tc.dash.Name, name))
   332  				if err != nil {
   333  					t.Errorf("Failed to create Path: %v", err)
   334  				}
   335  				client[*path] = fake.Stat{
   336  					Attrs: storage.ObjectAttrs{
   337  						Generation: group.gen,
   338  						Updated:    group.mod,
   339  					},
   340  				}
   341  			}
   342  			updateDashboard(context.Background(), client, tc.dash, &actual, finder, tabUpdater)
   343  			if diff := cmp.Diff(tc.expected, &actual, protocmp.Transform()); diff != "" {
   344  				t.Errorf("updateDashboard() got unexpected diff (-want +got):\n%s", diff)
   345  			}
   346  		})
   347  	}
   348  }
   349  
   350  func TestFilterDashboards(t *testing.T) {
   351  	cases := []struct {
   352  		name       string
   353  		dashboards map[string]*configpb.Dashboard
   354  		allowed    []string
   355  		want       map[string]*configpb.Dashboard
   356  	}{
   357  		{
   358  			name: "empty",
   359  		},
   360  		{
   361  			name: "basic",
   362  			dashboards: map[string]*configpb.Dashboard{
   363  				"hello": {Name: "hi"},
   364  			},
   365  			want: map[string]*configpb.Dashboard{
   366  				"hello": {Name: "hi"},
   367  			},
   368  		},
   369  		{
   370  			name: "zero",
   371  			dashboards: map[string]*configpb.Dashboard{
   372  				"hello": {Name: "hi"},
   373  			},
   374  			allowed: []string{"nothing"},
   375  			want:    map[string]*configpb.Dashboard{},
   376  		},
   377  		{
   378  			name: "both",
   379  			dashboards: map[string]*configpb.Dashboard{
   380  				"hello": {Name: "hi"},
   381  				"world": {Name: "there"},
   382  			},
   383  			allowed: []string{"hi", "there"},
   384  			want: map[string]*configpb.Dashboard{
   385  				"hello": {Name: "hi"},
   386  				"world": {Name: "there"},
   387  			},
   388  		},
   389  		{
   390  			name: "one",
   391  			dashboards: map[string]*configpb.Dashboard{
   392  				"hello": {Name: "hi"},
   393  				"drop":  {Name: "cuss-word"},
   394  				"world": {Name: "there"},
   395  			},
   396  			allowed: []string{"hi", "there", "drop"}, // target name, not key
   397  			want: map[string]*configpb.Dashboard{
   398  				"hello": {Name: "hi"},
   399  				"world": {Name: "there"},
   400  			},
   401  		},
   402  	}
   403  
   404  	for _, tc := range cases {
   405  		t.Run(tc.name, func(t *testing.T) {
   406  			var allowed stringset.Set
   407  			if tc.allowed != nil {
   408  				allowed = stringset.New(tc.allowed...)
   409  			}
   410  
   411  			got := filterDashboards(tc.dashboards, allowed)
   412  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
   413  				t.Errorf("filterDashboards() got unexpected diff (-want +got):\n%s", diff)
   414  			}
   415  		})
   416  	}
   417  }
   418  
   419  func TestStaleHours(t *testing.T) {
   420  	cases := []struct {
   421  		name     string
   422  		tab      *configpb.DashboardTab
   423  		expected time.Duration
   424  	}{
   425  		{
   426  			name:     "zero without an alert",
   427  			expected: 0,
   428  		},
   429  		{
   430  			name: "use defined hours when set",
   431  			tab: &configpb.DashboardTab{
   432  				AlertOptions: &configpb.DashboardTabAlertOptions{
   433  					AlertStaleResultsHours: 4,
   434  				},
   435  			},
   436  			expected: 4 * time.Hour,
   437  		},
   438  	}
   439  
   440  	for _, tc := range cases {
   441  		t.Run(tc.name, func(t *testing.T) {
   442  			if tc.tab == nil {
   443  				tc.tab = &configpb.DashboardTab{}
   444  			}
   445  			if actual := staleHours(tc.tab); actual != tc.expected {
   446  				t.Errorf("actual %v != expected %v", actual, tc.expected)
   447  			}
   448  		})
   449  	}
   450  }
   451  
   452  func gridBuf(grid *statepb.Grid) []byte {
   453  	buf, err := proto.Marshal(grid)
   454  	if err != nil {
   455  		panic(err)
   456  	}
   457  	return buf
   458  }
   459  
   460  func compress(buf []byte) []byte {
   461  	var zbuf bytes.Buffer
   462  	zw := zlib.NewWriter(&zbuf)
   463  	if _, err := zw.Write(buf); err != nil {
   464  		panic(err)
   465  	}
   466  	if err := zw.Close(); err != nil {
   467  		panic(err)
   468  	}
   469  	return zbuf.Bytes()
   470  }
   471  
   472  func TestUpdateTab(t *testing.T) {
   473  	now := time.Now()
   474  	cases := []struct {
   475  		name      string
   476  		tab       *configpb.DashboardTab
   477  		group     *configpb.TestGroup
   478  		grid      *statepb.Grid
   479  		mod       time.Time
   480  		gen       int64
   481  		gridError error
   482  		features  FeatureFlags
   483  		expected  *summarypb.DashboardTabSummary
   484  		err       bool
   485  	}{
   486  		{
   487  			name: "read grid error returns error",
   488  			tab: &configpb.DashboardTab{
   489  				TestGroupName: "foo",
   490  			},
   491  			group:     &configpb.TestGroup{},
   492  			mod:       now,
   493  			gen:       42,
   494  			gridError: errors.New("burninated"),
   495  			err:       true,
   496  		},
   497  		{
   498  			name: "basically works", // TODO(fejta): more better
   499  			tab: &configpb.DashboardTab{
   500  				Name:          "foo-tab",
   501  				TestGroupName: "foo-group",
   502  				AlertOptions: &configpb.DashboardTabAlertOptions{
   503  					AlertStaleResultsHours: 1,
   504  				},
   505  			},
   506  			group: &configpb.TestGroup{},
   507  			grid:  &statepb.Grid{},
   508  			mod:   now,
   509  			gen:   43,
   510  			expected: &summarypb.DashboardTabSummary{
   511  				DashboardTabName:    "foo-tab",
   512  				LastUpdateTimestamp: float64(now.Unix()),
   513  				Alert:               noRuns,
   514  				LatestGreen:         noGreens,
   515  				OverallStatus:       summarypb.DashboardTabSummary_STALE,
   516  				Status:              noRuns,
   517  				SummaryMetrics:      &summarypb.DashboardTabSummaryMetrics{},
   518  			},
   519  		},
   520  		{
   521  			name: "fuzzy flakiness configured and allowed",
   522  			tab: &configpb.DashboardTab{
   523  				Name:             "foo-tab",
   524  				TestGroupName:    "foo-group",
   525  				NumColumnsRecent: 4,
   526  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   527  					MaxAcceptableFlakiness: 50.0,
   528  				},
   529  			},
   530  			group: &configpb.TestGroup{},
   531  			grid: &statepb.Grid{
   532  				Rows: []*statepb.Row{
   533  					{
   534  						Name:    "test-1",
   535  						Results: []int32{int32(statuspb.TestStatus_PASS), 3, int32(statuspb.TestStatus_FAIL), 1},
   536  					},
   537  				},
   538  				Columns: []*statepb.Column{
   539  					{
   540  						Name:  "Uno",
   541  						Build: "1",
   542  					},
   543  					{
   544  						Name:  "Dos",
   545  						Build: "2",
   546  					},
   547  					{
   548  						Name:  "San",
   549  						Build: "3",
   550  					},
   551  					{
   552  						Name:  "Four",
   553  						Build: "4",
   554  					},
   555  				},
   556  			},
   557  			mod: now,
   558  			gen: 43,
   559  			features: FeatureFlags{
   560  				AllowFuzzyFlakiness: true,
   561  			},
   562  			expected: &summarypb.DashboardTabSummary{
   563  				DashboardTabName:    "foo-tab",
   564  				LastUpdateTimestamp: float64(now.Unix()),
   565  				OverallStatus:       summarypb.DashboardTabSummary_ACCEPTABLE,
   566  				LatestGreen:         "1",
   567  				Status:              "Tab stats: 3 of 4 (75.0%) recent columns passed (3 of 4 or 75.0% cells)\nStatus info: Recent flakiness (25.0%) over valid columns is within configured acceptable level of 50.0%.",
   568  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
   569  					CompletedColumns: 4,
   570  					PassingColumns:   3,
   571  				},
   572  			},
   573  		},
   574  		{
   575  			name: "fuzzy flakiness configured but not allowed",
   576  			tab: &configpb.DashboardTab{
   577  				Name:             "foo-tab",
   578  				TestGroupName:    "foo-group",
   579  				NumColumnsRecent: 4,
   580  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   581  					MaxAcceptableFlakiness: 50.0,
   582  				},
   583  			},
   584  			group: &configpb.TestGroup{},
   585  			grid: &statepb.Grid{
   586  				Rows: []*statepb.Row{
   587  					{
   588  						Name:    "test-1",
   589  						Results: []int32{int32(statuspb.TestStatus_PASS), 3, int32(statuspb.TestStatus_FAIL), 1},
   590  					},
   591  				},
   592  				Columns: []*statepb.Column{
   593  					{
   594  						Name:  "Uno",
   595  						Build: "1",
   596  					},
   597  					{
   598  						Name:  "Dos",
   599  						Build: "2",
   600  					},
   601  					{
   602  						Name:  "Three",
   603  						Build: "3",
   604  					},
   605  					{
   606  						Name:  "Quattro",
   607  						Build: "4",
   608  					},
   609  				},
   610  			},
   611  			mod: now,
   612  			gen: 43,
   613  			features: FeatureFlags{
   614  				AllowFuzzyFlakiness: false,
   615  			},
   616  			expected: &summarypb.DashboardTabSummary{
   617  				DashboardTabName:    "foo-tab",
   618  				LastUpdateTimestamp: float64(now.Unix()),
   619  				OverallStatus:       summarypb.DashboardTabSummary_FLAKY,
   620  				LatestGreen:         "1",
   621  				Status:              "Tab stats: 3 of 4 (75.0%) recent columns passed (3 of 4 or 75.0% cells)",
   622  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
   623  					CompletedColumns: 4,
   624  					PassingColumns:   3,
   625  				},
   626  			},
   627  		},
   628  		{
   629  			name: "ignored columns configured and allowed",
   630  			tab: &configpb.DashboardTab{
   631  				Name:             "foo-tab",
   632  				TestGroupName:    "foo-group",
   633  				NumColumnsRecent: 4,
   634  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   635  					IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
   636  						configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
   637  						configpb.DashboardTabStatusCustomizationOptions_CANCEL,
   638  					},
   639  				},
   640  			},
   641  			group: &configpb.TestGroup{},
   642  			grid: &statepb.Grid{
   643  				Rows: []*statepb.Row{
   644  					{
   645  						Name: "test-1",
   646  						Results: []int32{
   647  							int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
   648  							int32(statuspb.TestStatus_PASS), 3,
   649  						},
   650  					},
   651  					{
   652  						Name: "test-2",
   653  						Results: []int32{
   654  							int32(statuspb.TestStatus_FAIL), 1,
   655  							int32(statuspb.TestStatus_CANCEL), 1,
   656  							int32(statuspb.TestStatus_PASS), 2,
   657  						},
   658  					},
   659  				},
   660  				Columns: []*statepb.Column{
   661  					{
   662  						Name:  "Uno",
   663  						Build: "1",
   664  					},
   665  					{
   666  						Name:  "Dos",
   667  						Build: "2",
   668  					},
   669  					{
   670  						Name:  "San",
   671  						Build: "3",
   672  					},
   673  					{
   674  						Name:  "Chetyre",
   675  						Build: "4",
   676  					},
   677  				},
   678  			},
   679  			mod: now,
   680  			gen: 44,
   681  			features: FeatureFlags{
   682  				AllowIgnoredColumns: true,
   683  			},
   684  			expected: &summarypb.DashboardTabSummary{
   685  				DashboardTabName:    "foo-tab",
   686  				LastUpdateTimestamp: float64(now.Unix()),
   687  				OverallStatus:       summarypb.DashboardTabSummary_PASS,
   688  				LatestGreen:         "3",
   689  				Status:              "Tab stats: 2 of 4 (50.0%) recent columns passed (5 of 8 or 62.5% cells). 2 columns ignored",
   690  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
   691  					CompletedColumns: 4,
   692  					PassingColumns:   2,
   693  					IgnoredColumns:   2,
   694  				},
   695  			},
   696  		},
   697  		{
   698  			name: "ignored columns configured but not allowed",
   699  			tab: &configpb.DashboardTab{
   700  				Name:             "foo-tab",
   701  				TestGroupName:    "foo-group",
   702  				NumColumnsRecent: 4,
   703  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   704  					IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
   705  						configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
   706  						configpb.DashboardTabStatusCustomizationOptions_CANCEL,
   707  					},
   708  				},
   709  			},
   710  			group: &configpb.TestGroup{},
   711  			grid: &statepb.Grid{
   712  				Rows: []*statepb.Row{
   713  					{
   714  						Name: "test-1",
   715  						Results: []int32{
   716  							int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
   717  							int32(statuspb.TestStatus_PASS), 3,
   718  						},
   719  					},
   720  					{
   721  						Name: "test-2",
   722  						Results: []int32{
   723  							int32(statuspb.TestStatus_FAIL), 1,
   724  							int32(statuspb.TestStatus_CANCEL), 1,
   725  							int32(statuspb.TestStatus_PASS), 2,
   726  						},
   727  					},
   728  				},
   729  				Columns: []*statepb.Column{
   730  					{
   731  						Name:  "Uno",
   732  						Build: "1",
   733  					},
   734  					{
   735  						Name:  "Dos",
   736  						Build: "2",
   737  					},
   738  					{
   739  						Name:  "San",
   740  						Build: "3",
   741  					},
   742  					{
   743  						Name:  "Chetyre",
   744  						Build: "4",
   745  					},
   746  				},
   747  			},
   748  			mod: now,
   749  			gen: 44,
   750  			features: FeatureFlags{
   751  				AllowIgnoredColumns: false,
   752  			},
   753  			expected: &summarypb.DashboardTabSummary{
   754  				DashboardTabName:    "foo-tab",
   755  				LastUpdateTimestamp: float64(now.Unix()),
   756  				OverallStatus:       summarypb.DashboardTabSummary_FLAKY,
   757  				LatestGreen:         "3",
   758  				Status:              "Tab stats: 3 of 4 (75.0%) recent columns passed (5 of 8 or 62.5% cells)",
   759  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
   760  					CompletedColumns: 4,
   761  					PassingColumns:   3,
   762  					IgnoredColumns:   0,
   763  				},
   764  			},
   765  		},
   766  		{
   767  			name: "min required runs configured and allowed",
   768  			tab: &configpb.DashboardTab{
   769  				Name:             "foo-tab",
   770  				TestGroupName:    "foo-group",
   771  				NumColumnsRecent: 4,
   772  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   773  					MinAcceptableRuns: 5,
   774  				},
   775  			},
   776  			group: &configpb.TestGroup{},
   777  			grid: &statepb.Grid{
   778  				Rows: []*statepb.Row{
   779  					{
   780  						Name: "test-1",
   781  						Results: []int32{
   782  							int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
   783  							int32(statuspb.TestStatus_PASS), 3,
   784  						},
   785  					},
   786  					{
   787  						Name: "test-2",
   788  						Results: []int32{
   789  							int32(statuspb.TestStatus_FAIL), 1,
   790  							int32(statuspb.TestStatus_CANCEL), 1,
   791  							int32(statuspb.TestStatus_PASS), 2,
   792  						},
   793  					},
   794  				},
   795  				Columns: []*statepb.Column{
   796  					{
   797  						Name:  "Uno",
   798  						Build: "1",
   799  					},
   800  					{
   801  						Name:  "Dos",
   802  						Build: "2",
   803  					},
   804  					{
   805  						Name:  "San",
   806  						Build: "3",
   807  					},
   808  					{
   809  						Name:  "Chetyre",
   810  						Build: "4",
   811  					},
   812  				},
   813  			},
   814  			mod: now,
   815  			gen: 45,
   816  			features: FeatureFlags{
   817  				AllowMinNumberOfRuns: true,
   818  			},
   819  			expected: &summarypb.DashboardTabSummary{
   820  				DashboardTabName:    "foo-tab",
   821  				LastUpdateTimestamp: float64(now.Unix()),
   822  				OverallStatus:       summarypb.DashboardTabSummary_PENDING,
   823  				LatestGreen:         "3",
   824  				Status:              "Tab stats: 3 of 4 (75.0%) recent columns passed (5 of 8 or 62.5% cells)\nStatus info: Not enough runs",
   825  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
   826  					CompletedColumns: 4,
   827  					PassingColumns:   3,
   828  					IgnoredColumns:   0,
   829  				},
   830  			},
   831  		},
   832  		{
   833  			name: "min required runs configured but not allowed",
   834  			tab: &configpb.DashboardTab{
   835  				Name:             "foo-tab",
   836  				TestGroupName:    "foo-group",
   837  				NumColumnsRecent: 4,
   838  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   839  					MinAcceptableRuns: 5,
   840  				},
   841  			},
   842  			group: &configpb.TestGroup{},
   843  			grid: &statepb.Grid{
   844  				Rows: []*statepb.Row{
   845  					{
   846  						Name: "test-1",
   847  						Results: []int32{
   848  							int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
   849  							int32(statuspb.TestStatus_PASS), 3,
   850  						},
   851  					},
   852  					{
   853  						Name: "test-2",
   854  						Results: []int32{
   855  							int32(statuspb.TestStatus_FAIL), 1,
   856  							int32(statuspb.TestStatus_CANCEL), 1,
   857  							int32(statuspb.TestStatus_PASS), 2,
   858  						},
   859  					},
   860  				},
   861  				Columns: []*statepb.Column{
   862  					{
   863  						Name:  "Uno",
   864  						Build: "1",
   865  					},
   866  					{
   867  						Name:  "Dos",
   868  						Build: "2",
   869  					},
   870  					{
   871  						Name:  "San",
   872  						Build: "3",
   873  					},
   874  					{
   875  						Name:  "Chetyre",
   876  						Build: "4",
   877  					},
   878  				},
   879  			},
   880  			mod: now,
   881  			gen: 45,
   882  			features: FeatureFlags{
   883  				AllowMinNumberOfRuns: false,
   884  			},
   885  			expected: &summarypb.DashboardTabSummary{
   886  				DashboardTabName:    "foo-tab",
   887  				LastUpdateTimestamp: float64(now.Unix()),
   888  				OverallStatus:       summarypb.DashboardTabSummary_FLAKY,
   889  				LatestGreen:         "3",
   890  				Status:              "Tab stats: 3 of 4 (75.0%) recent columns passed (5 of 8 or 62.5% cells)",
   891  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
   892  					CompletedColumns: 4,
   893  					PassingColumns:   3,
   894  					IgnoredColumns:   0,
   895  				},
   896  			},
   897  		},
   898  		{
   899  			name: "not enough runs after ignoring",
   900  			tab: &configpb.DashboardTab{
   901  				Name:             "foo-tab",
   902  				TestGroupName:    "foo-group",
   903  				NumColumnsRecent: 4,
   904  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   905  					MinAcceptableRuns:      3,
   906  					MaxAcceptableFlakiness: 50.0,
   907  					IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
   908  						configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
   909  						configpb.DashboardTabStatusCustomizationOptions_BLOCKED,
   910  					},
   911  				},
   912  			},
   913  			group: &configpb.TestGroup{},
   914  			grid: &statepb.Grid{
   915  				Rows: []*statepb.Row{
   916  					{
   917  						Name: "test-1",
   918  						Results: []int32{
   919  							int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
   920  							int32(statuspb.TestStatus_PASS), 3,
   921  						},
   922  					},
   923  					{
   924  						Name: "test-2",
   925  						Results: []int32{
   926  							int32(statuspb.TestStatus_FAIL), 1,
   927  							int32(statuspb.TestStatus_BLOCKED), 1,
   928  							int32(statuspb.TestStatus_PASS), 2,
   929  						},
   930  					},
   931  				},
   932  				Columns: []*statepb.Column{
   933  					{
   934  						Name:  "Uno",
   935  						Build: "1",
   936  					},
   937  					{
   938  						Name:  "Dos",
   939  						Build: "2",
   940  					},
   941  					{
   942  						Name:  "San",
   943  						Build: "3",
   944  					},
   945  					{
   946  						Name:  "Chetyre",
   947  						Build: "4",
   948  					},
   949  				},
   950  			},
   951  			mod: now,
   952  			gen: 45,
   953  			features: FeatureFlags{
   954  				AllowMinNumberOfRuns: true,
   955  				AllowFuzzyFlakiness:  true,
   956  				AllowIgnoredColumns:  true,
   957  			},
   958  			expected: &summarypb.DashboardTabSummary{
   959  				DashboardTabName:    "foo-tab",
   960  				LastUpdateTimestamp: float64(now.Unix()),
   961  				OverallStatus:       summarypb.DashboardTabSummary_PENDING,
   962  				LatestGreen:         "3",
   963  				Status:              "Tab stats: 2 of 4 (50.0%) recent columns passed (5 of 8 or 62.5% cells). 2 columns ignored\nStatus info: Not enough runs",
   964  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
   965  					CompletedColumns: 4,
   966  					PassingColumns:   2,
   967  					IgnoredColumns:   2,
   968  				},
   969  			},
   970  		},
   971  		{
   972  			name: "acceptably flaky after ignoring",
   973  			tab: &configpb.DashboardTab{
   974  				Name:             "foo-tab",
   975  				TestGroupName:    "foo-group",
   976  				NumColumnsRecent: 4,
   977  				StatusCustomizationOptions: &configpb.DashboardTabStatusCustomizationOptions{
   978  					MaxAcceptableFlakiness: 35.0,
   979  					IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
   980  						configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
   981  						configpb.DashboardTabStatusCustomizationOptions_BLOCKED,
   982  					},
   983  				},
   984  			},
   985  			group: &configpb.TestGroup{},
   986  			grid: &statepb.Grid{
   987  				Rows: []*statepb.Row{
   988  					{
   989  						Name: "test-1",
   990  						Results: []int32{
   991  							int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
   992  							int32(statuspb.TestStatus_PASS), 3,
   993  						},
   994  					},
   995  					{
   996  						Name: "test-2",
   997  						Results: []int32{
   998  							int32(statuspb.TestStatus_FAIL), 2,
   999  							int32(statuspb.TestStatus_PASS), 2,
  1000  						},
  1001  					},
  1002  				},
  1003  				Columns: []*statepb.Column{
  1004  					{
  1005  						Name:  "Uno",
  1006  						Build: "1",
  1007  					},
  1008  					{
  1009  						Name:  "Dos",
  1010  						Build: "2",
  1011  					},
  1012  					{
  1013  						Name:  "San",
  1014  						Build: "3",
  1015  					},
  1016  					{
  1017  						Name:  "Chetyre",
  1018  						Build: "4",
  1019  					},
  1020  				},
  1021  			},
  1022  			mod: now,
  1023  			gen: 45,
  1024  			features: FeatureFlags{
  1025  				AllowMinNumberOfRuns: true,
  1026  				AllowFuzzyFlakiness:  true,
  1027  				AllowIgnoredColumns:  true,
  1028  			},
  1029  			expected: &summarypb.DashboardTabSummary{
  1030  				DashboardTabName:    "foo-tab",
  1031  				LastUpdateTimestamp: float64(now.Unix()),
  1032  				OverallStatus:       summarypb.DashboardTabSummary_ACCEPTABLE,
  1033  				LatestGreen:         "3",
  1034  				Status:              "Tab stats: 2 of 4 (50.0%) recent columns passed (5 of 8 or 62.5% cells). 1 columns ignored\nStatus info: Recent flakiness (33.3%) over valid columns is within configured acceptable level of 35.0%.",
  1035  				SummaryMetrics: &summarypb.DashboardTabSummaryMetrics{
  1036  					CompletedColumns: 4,
  1037  					PassingColumns:   2,
  1038  					IgnoredColumns:   1,
  1039  				},
  1040  			},
  1041  		},
  1042  		{
  1043  			name: "missing grid returns a blank summary",
  1044  			tab: &configpb.DashboardTab{
  1045  				Name: "you know",
  1046  			},
  1047  			group:     &configpb.TestGroup{},
  1048  			gridError: fmt.Errorf("oh yeah: %w", storage.ErrObjectNotExist),
  1049  			err:       true,
  1050  		},
  1051  	}
  1052  
  1053  	for _, tc := range cases {
  1054  		t.Run(tc.name, func(t *testing.T) {
  1055  			if tc.tab == nil {
  1056  				tc.tab = &configpb.DashboardTab{}
  1057  			}
  1058  			reader := func(_ context.Context) (io.ReadCloser, time.Time, int64, error) {
  1059  				if tc.gridError != nil {
  1060  					return nil, time.Time{}, 0, tc.gridError
  1061  				}
  1062  				return ioutil.NopCloser(bytes.NewBuffer(compress(gridBuf(tc.grid)))), tc.mod, tc.gen, nil
  1063  			}
  1064  			actual, err := updateTab(context.Background(), tc.tab, tc.group, reader, tc.features)
  1065  			switch {
  1066  			case err != nil:
  1067  				if !tc.err {
  1068  					t.Errorf("unexpected error: %v", err)
  1069  				}
  1070  			case tc.err:
  1071  				t.Errorf("failed to receive expected error")
  1072  			case !proto.Equal(actual, tc.expected):
  1073  				t.Errorf("actual summary: %s != expected %s", actual, tc.expected)
  1074  			}
  1075  		})
  1076  	}
  1077  }
  1078  
  1079  func TestReadGrid(t *testing.T) {
  1080  	cases := []struct {
  1081  		name         string
  1082  		reader       io.Reader
  1083  		err          error
  1084  		expectedGrid *statepb.Grid
  1085  		expectErr    bool
  1086  	}{
  1087  		{
  1088  			name:      "error opening returns error",
  1089  			err:       errors.New("open failed"),
  1090  			expectErr: true,
  1091  		},
  1092  		{
  1093  			name: "return error when state is not compressed",
  1094  			reader: bytes.NewBuffer(gridBuf(&statepb.Grid{
  1095  				LastTimeUpdated: 444,
  1096  			})),
  1097  			expectErr: true,
  1098  		},
  1099  		{
  1100  			name:      "return error when compressed object is not a grid proto",
  1101  			reader:    bytes.NewBuffer(compress([]byte("hello"))),
  1102  			expectErr: true,
  1103  		},
  1104  		{
  1105  			name: "return error when compressed proto is truncated",
  1106  			reader: bytes.NewBuffer(compress(gridBuf(&statepb.Grid{
  1107  				Columns: []*statepb.Column{
  1108  					{
  1109  						Build:      "really long info",
  1110  						Name:       "weeee",
  1111  						HotlistIds: "super exciting",
  1112  					},
  1113  				},
  1114  				LastTimeUpdated: 555,
  1115  			}))[:10]),
  1116  			expectErr: true,
  1117  		},
  1118  		{
  1119  			name: "successfully parse compressed grid",
  1120  			reader: bytes.NewBuffer(compress(gridBuf(&statepb.Grid{
  1121  				LastTimeUpdated: 555,
  1122  			}))),
  1123  			expectedGrid: &statepb.Grid{
  1124  				LastTimeUpdated: 555,
  1125  			},
  1126  		},
  1127  	}
  1128  
  1129  	for _, tc := range cases {
  1130  		now := time.Now()
  1131  		t.Run(tc.name, func(t *testing.T) {
  1132  			const gen = 42
  1133  			reader := func(_ context.Context) (io.ReadCloser, time.Time, int64, error) {
  1134  				if tc.err != nil {
  1135  					return nil, time.Time{}, 0, tc.err
  1136  				}
  1137  				return ioutil.NopCloser(tc.reader), now, gen, nil
  1138  			}
  1139  
  1140  			actualGrid, aT, aGen, err := readGrid(context.Background(), reader)
  1141  
  1142  			switch {
  1143  			case err != nil:
  1144  				if !tc.expectErr {
  1145  					t.Errorf("unexpected error: %v", err)
  1146  				}
  1147  			case tc.expectErr:
  1148  				t.Error("failed to receive expected error")
  1149  			case !proto.Equal(actualGrid, tc.expectedGrid):
  1150  				t.Errorf("actual state: %#v != expected %#v", actualGrid, tc.expectedGrid)
  1151  			case !now.Equal(aT):
  1152  				t.Errorf("actual modified: %v != expected %v", aT, now)
  1153  			case aGen != gen:
  1154  				t.Errorf("actual generation: %d != expected %d", aGen, gen)
  1155  			}
  1156  		})
  1157  	}
  1158  }
  1159  
  1160  func TestRecentColumns(t *testing.T) {
  1161  	cases := []struct {
  1162  		name     string
  1163  		tab      int32
  1164  		group    int32
  1165  		expected int
  1166  	}{
  1167  		{
  1168  			name:     "prefer tab over group",
  1169  			tab:      1,
  1170  			group:    2,
  1171  			expected: 1,
  1172  		},
  1173  		{
  1174  			name:     "use group if tab is empty",
  1175  			group:    9,
  1176  			expected: 9,
  1177  		},
  1178  		{
  1179  			name:     "use default when both are empty",
  1180  			expected: 5,
  1181  		},
  1182  	}
  1183  
  1184  	for _, tc := range cases {
  1185  		t.Run(tc.name, func(t *testing.T) {
  1186  			tabCfg := &configpb.DashboardTab{
  1187  				NumColumnsRecent: tc.tab,
  1188  			}
  1189  			groupCfg := &configpb.TestGroup{
  1190  				NumColumnsRecent: tc.group,
  1191  			}
  1192  			if actual := recentColumns(tabCfg, groupCfg); actual != tc.expected {
  1193  				t.Errorf("actual %d != expected %d", actual, tc.expected)
  1194  			}
  1195  		})
  1196  	}
  1197  }
  1198  
  1199  func TestAllLinkedIssues(t *testing.T) {
  1200  	cases := []struct {
  1201  		name string
  1202  		rows []*statepb.Row
  1203  		want []string
  1204  	}{
  1205  		{
  1206  			name: "no rows",
  1207  			rows: []*statepb.Row{},
  1208  			want: []string{},
  1209  		},
  1210  		{
  1211  			name: "rows with no linked issues",
  1212  			rows: []*statepb.Row{
  1213  				{
  1214  					Name:    "test-1",
  1215  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1216  				},
  1217  				{
  1218  					Name:    "test-2",
  1219  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1220  				},
  1221  				{
  1222  					Name:    "test-3",
  1223  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1224  				},
  1225  			},
  1226  			want: []string{},
  1227  		},
  1228  		{
  1229  			name: "multiple linked issues",
  1230  			rows: []*statepb.Row{
  1231  				{
  1232  					Name:    "test-1",
  1233  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1234  					Issues:  []string{"1", "2"},
  1235  				},
  1236  				{
  1237  					Name:    "test-2",
  1238  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1239  					Issues:  []string{"5"},
  1240  				},
  1241  				{
  1242  					Name:    "test-3",
  1243  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1244  					Issues:  []string{"10", "7"},
  1245  				},
  1246  			},
  1247  			want: []string{"1", "2", "5", "7", "10"},
  1248  		},
  1249  		{
  1250  			name: "multiple linked issues with duplicates",
  1251  			rows: []*statepb.Row{
  1252  				{
  1253  					Name:    "test-1",
  1254  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1255  					Issues:  []string{"1", "2"},
  1256  				},
  1257  				{
  1258  					Name:    "test-2",
  1259  					Results: []int32{int32(statuspb.TestStatus_PASS), 10},
  1260  					Issues:  []string{"2", "3"},
  1261  				},
  1262  			},
  1263  			want: []string{"1", "2", "3"},
  1264  		},
  1265  	}
  1266  
  1267  	for _, tc := range cases {
  1268  		t.Run(tc.name, func(t *testing.T) {
  1269  			got := allLinkedIssues(tc.rows)
  1270  			sort.Strings(got)
  1271  			strSort := cmpopts.SortSlices(func(a, b string) bool { return a < b })
  1272  			if diff := cmp.Diff(tc.want, got, strSort); diff != "" {
  1273  				t.Errorf("allLinkedIssues() unexpected diff (-want +got): %s", diff)
  1274  			}
  1275  		})
  1276  	}
  1277  }
  1278  
  1279  func TestFirstFilled(t *testing.T) {
  1280  	cases := []struct {
  1281  		name     string
  1282  		values   []int32
  1283  		expected int
  1284  	}{
  1285  		{
  1286  			name: "zero by default",
  1287  		},
  1288  		{
  1289  			name:     "first non-zero value",
  1290  			values:   []int32{0, 1, 2},
  1291  			expected: 1,
  1292  		},
  1293  	}
  1294  
  1295  	for _, tc := range cases {
  1296  		t.Run(tc.name, func(t *testing.T) {
  1297  			if actual := firstFilled(tc.values...); actual != tc.expected {
  1298  				t.Errorf("actual %d != expected %d", actual, tc.expected)
  1299  			}
  1300  		})
  1301  	}
  1302  }
  1303  
  1304  func TestFilterMethods(t *testing.T) {
  1305  	cases := []struct {
  1306  		name     string
  1307  		rows     []*statepb.Row
  1308  		recent   int
  1309  		expected []*statepb.Row
  1310  		err      bool
  1311  	}{
  1312  		{
  1313  			name: "tolerates nil inputs",
  1314  		},
  1315  		{
  1316  			name: "basically works",
  1317  			rows: []*statepb.Row{
  1318  				{
  1319  					Name: "okay",
  1320  					Id:   "cool",
  1321  				},
  1322  			},
  1323  			expected: []*statepb.Row{
  1324  				{
  1325  					Name: "okay",
  1326  					Id:   "cool",
  1327  				},
  1328  			},
  1329  		},
  1330  		{
  1331  			name: "exclude all test methods",
  1332  			rows: []*statepb.Row{
  1333  				{
  1334  					Name: "test-1",
  1335  					Id:   "test-1",
  1336  				},
  1337  				{
  1338  					Name: "method-1",
  1339  					Id:   "test-1@TESTGRID@method-1",
  1340  				},
  1341  				{
  1342  					Name: "method-2",
  1343  					Id:   "test-1@TESTGRID@method-2",
  1344  				},
  1345  				{
  1346  					Name: "test-2",
  1347  					Id:   "test-2",
  1348  				},
  1349  				{
  1350  					Name: "test-2@TESTGRID@method-1",
  1351  					Id:   "method-1",
  1352  				},
  1353  			},
  1354  			expected: []*statepb.Row{
  1355  				{
  1356  					Name: "test-1",
  1357  					Id:   "test-1",
  1358  				},
  1359  				{
  1360  					Name: "test-2",
  1361  					Id:   "test-2",
  1362  				},
  1363  			},
  1364  		},
  1365  	}
  1366  
  1367  	for _, tc := range cases {
  1368  		t.Run(tc.name, func(t *testing.T) {
  1369  			for _, r := range tc.rows {
  1370  				if r.Results == nil {
  1371  					r.Results = []int32{int32(statuspb.TestStatus_PASS), 100}
  1372  				}
  1373  			}
  1374  			for _, r := range tc.expected {
  1375  				if r.Results == nil {
  1376  					r.Results = []int32{int32(statuspb.TestStatus_PASS), 100}
  1377  				}
  1378  			}
  1379  			actual := filterMethods(tc.rows)
  1380  
  1381  			if !cmp.Equal(actual, tc.expected, protocmp.Transform()) {
  1382  				t.Errorf("%s != expected %s", actual, tc.expected)
  1383  			}
  1384  		})
  1385  	}
  1386  }
  1387  
  1388  func TestRecentRows(t *testing.T) {
  1389  	const recent = 10
  1390  	cases := []struct {
  1391  		name     string
  1392  		rows     []*statepb.Row
  1393  		expected []string
  1394  	}{
  1395  		{
  1396  			name: "basically works",
  1397  		},
  1398  		{
  1399  			name: "skip row with nil results",
  1400  			rows: []*statepb.Row{
  1401  				{
  1402  					Name:    "include",
  1403  					Results: []int32{int32(statuspb.TestStatus_PASS), recent},
  1404  				},
  1405  				{
  1406  					Name: "skip-nil-results",
  1407  				},
  1408  			},
  1409  			expected: []string{"include"},
  1410  		},
  1411  		{
  1412  			name: "skip row with no recent results",
  1413  			rows: []*statepb.Row{
  1414  				{
  1415  					Name:    "include",
  1416  					Results: []int32{int32(statuspb.TestStatus_PASS), recent},
  1417  				},
  1418  				{
  1419  					Name:    "skip-this-one-with-no-recent-results",
  1420  					Results: []int32{int32(statuspb.TestStatus_NO_RESULT), recent},
  1421  				},
  1422  			},
  1423  			expected: []string{"include"},
  1424  		},
  1425  		{
  1426  			name: "include rows missing some recent results",
  1427  			rows: []*statepb.Row{
  1428  				{
  1429  					Name: "head skips",
  1430  					Results: []int32{
  1431  						int32(statuspb.TestStatus_NO_RESULT), recent - 1,
  1432  						int32(statuspb.TestStatus_PASS_WITH_SKIPS), recent,
  1433  					},
  1434  				},
  1435  				{
  1436  					Name: "tail skips",
  1437  					Results: []int32{
  1438  						int32(statuspb.TestStatus_FLAKY), recent - 1,
  1439  						int32(statuspb.TestStatus_NO_RESULT), recent,
  1440  					},
  1441  				},
  1442  				{
  1443  					Name: "middle skips",
  1444  					Results: []int32{
  1445  						int32(statuspb.TestStatus_FAIL), 1,
  1446  						int32(statuspb.TestStatus_NO_RESULT), recent - 2,
  1447  						int32(statuspb.TestStatus_PASS), 1,
  1448  					},
  1449  				},
  1450  			},
  1451  			expected: []string{
  1452  				"head skips",
  1453  				"tail skips",
  1454  				"middle skips",
  1455  			},
  1456  		},
  1457  	}
  1458  
  1459  	for _, tc := range cases {
  1460  		t.Run(tc.name, func(t *testing.T) {
  1461  			actualRows := recentRows(tc.rows, recent)
  1462  
  1463  			var actual []string
  1464  			for _, r := range actualRows {
  1465  				actual = append(actual, r.Name)
  1466  			}
  1467  			if !cmp.Equal(actual, tc.expected, protocmp.Transform()) {
  1468  				t.Errorf("%s != expected %s", actual, tc.expected)
  1469  			}
  1470  		})
  1471  	}
  1472  }
  1473  
  1474  func TestLatestRun(t *testing.T) {
  1475  	cases := []struct {
  1476  		name         string
  1477  		cols         []*statepb.Column
  1478  		expectedTime time.Time
  1479  		expectedSecs int64
  1480  	}{
  1481  		{
  1482  			name: "basically works",
  1483  		},
  1484  		{
  1485  			name: "zero started returns zero time",
  1486  			cols: []*statepb.Column{
  1487  				{},
  1488  			},
  1489  		},
  1490  		{
  1491  			name: "return first time in unix",
  1492  			cols: []*statepb.Column{
  1493  				{
  1494  					Started: 333333,
  1495  				},
  1496  				{
  1497  					Started: 222222,
  1498  				},
  1499  			},
  1500  			expectedTime: time.Unix(333, 333000000),
  1501  			expectedSecs: 333,
  1502  		},
  1503  	}
  1504  
  1505  	for _, tc := range cases {
  1506  		t.Run(tc.name, func(t *testing.T) {
  1507  			when, s := latestRun(tc.cols)
  1508  			if !when.Equal(tc.expectedTime) {
  1509  				t.Errorf("time %v != expected %v", when, tc.expectedTime)
  1510  			}
  1511  			if s != tc.expectedSecs {
  1512  				t.Errorf("seconds %d != expected %d", s, tc.expectedSecs)
  1513  			}
  1514  		})
  1515  	}
  1516  }
  1517  
  1518  func TestStaleAlert(t *testing.T) {
  1519  	cases := []struct {
  1520  		name  string
  1521  		mod   time.Time
  1522  		ran   time.Time
  1523  		dur   time.Duration
  1524  		rows  int
  1525  		alert bool
  1526  	}{
  1527  		{
  1528  			name: "basically works",
  1529  			mod:  time.Now().Add(-5 * time.Minute),
  1530  			ran:  time.Now().Add(-10 * time.Minute),
  1531  			dur:  time.Hour,
  1532  			rows: 10,
  1533  		},
  1534  		{
  1535  			name:  "unmodified alerts",
  1536  			mod:   time.Now().Add(-5 * time.Hour),
  1537  			ran:   time.Now(),
  1538  			dur:   time.Hour,
  1539  			rows:  10,
  1540  			alert: true,
  1541  		},
  1542  		{
  1543  			name:  "no recent runs alerts",
  1544  			mod:   time.Now(),
  1545  			ran:   time.Now().Add(-5 * time.Hour),
  1546  			dur:   time.Hour,
  1547  			rows:  10,
  1548  			alert: true,
  1549  		},
  1550  		{
  1551  			name:  "no runs alerts",
  1552  			mod:   time.Now(),
  1553  			dur:   time.Hour,
  1554  			rows:  10,
  1555  			alert: true,
  1556  		},
  1557  		{
  1558  			name:  "no rows alerts",
  1559  			mod:   time.Now().Add(-5 * time.Minute),
  1560  			ran:   time.Now().Add(-10 * time.Minute),
  1561  			dur:   time.Hour,
  1562  			rows:  0,
  1563  			alert: true,
  1564  		},
  1565  		{
  1566  			name: "no runs w/ stale hours not configured does not alert",
  1567  			mod:  time.Now(),
  1568  		},
  1569  		{
  1570  			name:  "no state w/ stale hours not configured alerts",
  1571  			alert: true,
  1572  		},
  1573  	}
  1574  
  1575  	for _, tc := range cases {
  1576  		t.Run(tc.name, func(t *testing.T) {
  1577  			actual := staleAlert(tc.mod, tc.ran, tc.dur, tc.rows)
  1578  			if actual != "" && !tc.alert {
  1579  				t.Errorf("unexpected stale alert: %s", actual)
  1580  			}
  1581  			if actual == "" && tc.alert {
  1582  				t.Errorf("failed to create a stale alert")
  1583  			}
  1584  		})
  1585  	}
  1586  }
  1587  
  1588  func TestFailingTestSummaries(t *testing.T) {
  1589  	defaultTemplate := &configpb.LinkTemplate{
  1590  		Url: "http://test.com/view/<workflow-name>/<workflow-id>",
  1591  		Options: []*configpb.LinkOptionsTemplate{
  1592  			{
  1593  				Key:   "test",
  1594  				Value: "<test-name>@<test-id>",
  1595  			},
  1596  			{
  1597  				Key:   "path",
  1598  				Value: "<encode:<gcs_prefix>>",
  1599  			},
  1600  		},
  1601  	}
  1602  	defaultGcsPrefix := "my-bucket/logs/cool-job"
  1603  	defaultColumnHeader := []*configpb.TestGroup_ColumnHeader{
  1604  		{
  1605  			Property: "foo",
  1606  		},
  1607  		{
  1608  			Label: "hello",
  1609  		},
  1610  	}
  1611  	cases := []struct {
  1612  		name         string
  1613  		template     *configpb.LinkTemplate
  1614  		gcsPrefix    string
  1615  		columnHeader []*configpb.TestGroup_ColumnHeader
  1616  		rows         []*statepb.Row
  1617  		expected     []*summarypb.FailingTestSummary
  1618  	}{
  1619  		{
  1620  			name:         "do not alert by default",
  1621  			template:     defaultTemplate,
  1622  			gcsPrefix:    defaultGcsPrefix,
  1623  			columnHeader: defaultColumnHeader,
  1624  			rows: []*statepb.Row{
  1625  				{},
  1626  				{},
  1627  			},
  1628  		},
  1629  		{
  1630  			name:      "alert when rows have alerts",
  1631  			template:  defaultTemplate,
  1632  			gcsPrefix: defaultGcsPrefix,
  1633  			columnHeader: []*configpb.TestGroup_ColumnHeader{
  1634  				{
  1635  					Property: "foo",
  1636  				},
  1637  			},
  1638  			rows: []*statepb.Row{
  1639  				{},
  1640  				{
  1641  					Name:   "foo-name",
  1642  					Id:     "foo-target",
  1643  					Issues: []string{"1234", "5678"},
  1644  					AlertInfo: &statepb.AlertInfo{
  1645  						FailBuildId:       "bad",
  1646  						LatestFailBuildId: "still-bad",
  1647  						PassBuildId:       "good",
  1648  						FailCount:         6,
  1649  						BuildLink:         "to the past",
  1650  						BuildLinkText:     "hyrule",
  1651  						BuildUrlText:      "of sandwich",
  1652  						FailureMessage:    "pop tart",
  1653  						Properties: map[string]string{
  1654  							"ham": "eggs",
  1655  						},
  1656  						CustomColumnHeaders: map[string]string{
  1657  							"foo": "bar",
  1658  						},
  1659  						HotlistIds: []string{},
  1660  					},
  1661  				},
  1662  				{},
  1663  				{
  1664  					Name:   "bar-name",
  1665  					Id:     "bar-target",
  1666  					Issues: []string{"1234"},
  1667  					AlertInfo: &statepb.AlertInfo{
  1668  						FailBuildId:       "fbi",
  1669  						LatestFailBuildId: "lfbi",
  1670  						PassBuildId:       "pbi",
  1671  						FailTestId:        "819283y823-1232813",
  1672  						LatestFailTestId:  "920394z934-2343924",
  1673  						FailCount:         1,
  1674  						BuildLink:         "bl",
  1675  						BuildLinkText:     "blt",
  1676  						BuildUrlText:      "but",
  1677  						FailureMessage:    "fm",
  1678  						Properties: map[string]string{
  1679  							"foo":   "bar",
  1680  							"hello": "lots",
  1681  						},
  1682  						CustomColumnHeaders: map[string]string{
  1683  							"foo": "notbar",
  1684  						},
  1685  						HotlistIds: []string{"111", "222"},
  1686  					},
  1687  				},
  1688  				{},
  1689  			},
  1690  			expected: []*summarypb.FailingTestSummary{
  1691  				{
  1692  					DisplayName:        "foo-name",
  1693  					TestName:           "foo-target",
  1694  					FailBuildId:        "bad",
  1695  					LatestFailBuildId:  "still-bad",
  1696  					PassBuildId:        "good",
  1697  					FailCount:          6,
  1698  					BuildLink:          "to the past",
  1699  					BuildLinkText:      "hyrule",
  1700  					BuildUrlText:       "of sandwich",
  1701  					FailureMessage:     "pop tart",
  1702  					FailTestLink:       " foo-target",
  1703  					LatestFailTestLink: " foo-target",
  1704  					LinkedBugs:         []string{"1234", "5678"},
  1705  					Properties: map[string]string{
  1706  						"ham": "eggs",
  1707  					},
  1708  					CustomColumnHeaders: map[string]string{
  1709  						"foo": "bar",
  1710  					},
  1711  					HotlistIds: []string{},
  1712  				},
  1713  				{
  1714  					DisplayName:        "bar-name",
  1715  					TestName:           "bar-target",
  1716  					FailBuildId:        "fbi",
  1717  					LatestFailBuildId:  "lfbi",
  1718  					PassBuildId:        "pbi",
  1719  					FailCount:          1,
  1720  					BuildLink:          "bl",
  1721  					BuildLinkText:      "blt",
  1722  					BuildUrlText:       "but",
  1723  					FailureMessage:     "fm",
  1724  					FailTestLink:       "819283y823-1232813 bar-target",
  1725  					LatestFailTestLink: "920394z934-2343924 bar-target",
  1726  					LinkedBugs:         []string{"1234"},
  1727  					Properties: map[string]string{
  1728  						"foo":   "bar",
  1729  						"hello": "lots",
  1730  					},
  1731  					CustomColumnHeaders: map[string]string{
  1732  						"foo": "notbar",
  1733  					},
  1734  					HotlistIds: []string{"111", "222"},
  1735  				},
  1736  			},
  1737  		},
  1738  	}
  1739  
  1740  	for _, tc := range cases {
  1741  		t.Run(tc.name, func(t *testing.T) {
  1742  			actual := failingTestSummaries(tc.rows, tc.template, tc.gcsPrefix, tc.columnHeader)
  1743  			if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" {
  1744  				t.Errorf("failingTestSummaries() (-want, +got): %s", diff)
  1745  			}
  1746  		})
  1747  	}
  1748  }
  1749  
  1750  func TestOverallStatus(t *testing.T) {
  1751  	cases := []struct {
  1752  		name     string
  1753  		rows     []*statepb.Row
  1754  		recent   int
  1755  		stale    string
  1756  		broken   bool
  1757  		alerts   bool
  1758  		features FeatureFlags
  1759  		colCells gridStats
  1760  		opts     *configpb.DashboardTabStatusCustomizationOptions
  1761  		expected summarypb.DashboardTabSummary_TabStatus
  1762  	}{
  1763  		{
  1764  			name:     "unknown by default",
  1765  			expected: summarypb.DashboardTabSummary_UNKNOWN,
  1766  		},
  1767  		{
  1768  			name:     "stale joke results in stale summary",
  1769  			stale:    "joke",
  1770  			expected: summarypb.DashboardTabSummary_STALE,
  1771  		},
  1772  		{
  1773  			name:     "alerts result in failure",
  1774  			alerts:   true,
  1775  			expected: summarypb.DashboardTabSummary_FAIL,
  1776  		},
  1777  		{
  1778  			name:     "prefer stale over failure",
  1779  			stale:    "potato chip",
  1780  			alerts:   true,
  1781  			expected: summarypb.DashboardTabSummary_STALE,
  1782  		},
  1783  		{
  1784  			name:   "completed results result in pass",
  1785  			recent: 1,
  1786  			rows: []*statepb.Row{
  1787  				{
  1788  					Results: []int32{int32(statuspb.TestStatus_PASS), 1},
  1789  				},
  1790  			},
  1791  			expected: summarypb.DashboardTabSummary_PASS,
  1792  		},
  1793  		{
  1794  			name:   "non-passing results without an alert results in flaky",
  1795  			recent: 1,
  1796  			rows: []*statepb.Row{
  1797  				{
  1798  					Results: []int32{int32(statuspb.TestStatus_FAIL), 1},
  1799  				},
  1800  			},
  1801  			expected: summarypb.DashboardTabSummary_FLAKY,
  1802  		},
  1803  		{
  1804  			name:   "incomplete passing results", // ignore them
  1805  			recent: 5,
  1806  			rows: []*statepb.Row{
  1807  				{
  1808  					Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 1},
  1809  				},
  1810  				{
  1811  					Results: []int32{int32(statuspb.TestStatus_PASS), 3},
  1812  				},
  1813  				{
  1814  					Results: []int32{int32(statuspb.TestStatus_RUNNING), 2},
  1815  				},
  1816  				{
  1817  					Results: []int32{int32(statuspb.TestStatus_PASS), 2},
  1818  				},
  1819  			},
  1820  			expected: summarypb.DashboardTabSummary_PASS,
  1821  		},
  1822  		{
  1823  			name:   "incomplete flaky results", // ignore them
  1824  			recent: 5,
  1825  			rows: []*statepb.Row{
  1826  				{
  1827  					Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 1},
  1828  				},
  1829  				{
  1830  					Results: []int32{int32(statuspb.TestStatus_PASS), 3},
  1831  				},
  1832  				{
  1833  					Results: []int32{int32(statuspb.TestStatus_RUNNING), 2},
  1834  				},
  1835  				{
  1836  					Results: []int32{int32(statuspb.TestStatus_FAIL), 2},
  1837  				},
  1838  			},
  1839  			expected: summarypb.DashboardTabSummary_FLAKY,
  1840  		},
  1841  		{
  1842  			name:   "ignore old failures",
  1843  			recent: 1,
  1844  			rows: []*statepb.Row{
  1845  				{
  1846  					Results: []int32{
  1847  						int32(statuspb.TestStatus_PASS), 3,
  1848  						int32(statuspb.TestStatus_FAIL), 5,
  1849  					},
  1850  				},
  1851  			},
  1852  			expected: summarypb.DashboardTabSummary_PASS,
  1853  		},
  1854  		{
  1855  			name:   "dropped columns", // should not impact status
  1856  			recent: 1,
  1857  			rows: []*statepb.Row{
  1858  				{
  1859  					Name: "current",
  1860  					Results: []int32{
  1861  						int32(statuspb.TestStatus_PASS), 2,
  1862  					},
  1863  				},
  1864  				{
  1865  					Name: "ignore dropped",
  1866  					Results: []int32{
  1867  						int32(statuspb.TestStatus_NO_RESULT), 1,
  1868  						int32(statuspb.TestStatus_FAIL), 1,
  1869  					},
  1870  				},
  1871  			},
  1872  			expected: summarypb.DashboardTabSummary_PASS,
  1873  		},
  1874  		{
  1875  			name:   "running", // do not count as recent
  1876  			recent: 1,
  1877  			rows: []*statepb.Row{
  1878  				{
  1879  					Name: "pass",
  1880  					Results: []int32{
  1881  						int32(statuspb.TestStatus_PASS), 2,
  1882  					},
  1883  				},
  1884  				{
  1885  					Name: "running",
  1886  					Results: []int32{
  1887  						int32(statuspb.TestStatus_RUNNING), 1,
  1888  						int32(statuspb.TestStatus_PASS), 1,
  1889  					},
  1890  				},
  1891  				{
  1892  					Name: "flake",
  1893  					Results: []int32{
  1894  						int32(statuspb.TestStatus_PASS), 1,
  1895  						int32(statuspb.TestStatus_FAIL), 1,
  1896  					},
  1897  				},
  1898  			},
  1899  			expected: summarypb.DashboardTabSummary_FLAKY,
  1900  		},
  1901  		{
  1902  			name:   "partial results work",
  1903  			recent: 50,
  1904  			rows: []*statepb.Row{
  1905  				{
  1906  					Results: []int32{int32(statuspb.TestStatus_PASS), 1},
  1907  				},
  1908  			},
  1909  			expected: summarypb.DashboardTabSummary_PASS,
  1910  		},
  1911  		{
  1912  			name:   "coalesce passes",
  1913  			recent: 1,
  1914  			rows: []*statepb.Row{
  1915  				{
  1916  					Results: []int32{int32(statuspb.TestStatus_PASS_WITH_SKIPS), 1},
  1917  				},
  1918  			},
  1919  			expected: summarypb.DashboardTabSummary_PASS,
  1920  		},
  1921  		{
  1922  			name:   "broken cycle",
  1923  			recent: 1,
  1924  			rows: []*statepb.Row{
  1925  				{
  1926  					Results: []int32{int32(statuspb.TestStatus_PASS_WITH_SKIPS), 1},
  1927  				},
  1928  			},
  1929  			broken:   true,
  1930  			expected: summarypb.DashboardTabSummary_BROKEN,
  1931  		},
  1932  		{
  1933  			name:   "more runs required but flag not enabled",
  1934  			recent: 4,
  1935  			rows: []*statepb.Row{
  1936  				{
  1937  					Results: []int32{
  1938  						int32(statuspb.TestStatus_PASS_WITH_SKIPS), 2,
  1939  						int32(statuspb.TestStatus_FAIL), 2,
  1940  					},
  1941  				},
  1942  			},
  1943  			features: FeatureFlags{
  1944  				AllowMinNumberOfRuns: false,
  1945  			},
  1946  			colCells: gridStats{
  1947  				ignoredCols:   2,
  1948  				completedCols: 4,
  1949  			},
  1950  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  1951  				MinAcceptableRuns: 3,
  1952  			},
  1953  			expected: summarypb.DashboardTabSummary_FLAKY,
  1954  		},
  1955  		{
  1956  			name:   "more runs required with flag enabled",
  1957  			recent: 4,
  1958  			rows: []*statepb.Row{
  1959  				{
  1960  					Results: []int32{
  1961  						int32(statuspb.TestStatus_PASS_WITH_SKIPS), 2,
  1962  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2,
  1963  					},
  1964  				},
  1965  			},
  1966  			features: FeatureFlags{
  1967  				AllowMinNumberOfRuns: true,
  1968  			},
  1969  			colCells: gridStats{
  1970  				ignoredCols:   2,
  1971  				completedCols: 4,
  1972  			},
  1973  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  1974  				MinAcceptableRuns: 3,
  1975  			},
  1976  			expected: summarypb.DashboardTabSummary_PENDING,
  1977  		},
  1978  		{
  1979  			name:   "neutral statuses ignored but flag not enabled",
  1980  			recent: 4,
  1981  			rows: []*statepb.Row{
  1982  				{
  1983  					Results: []int32{
  1984  						int32(statuspb.TestStatus_PASS), 2,
  1985  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2,
  1986  					},
  1987  				},
  1988  			},
  1989  			features: FeatureFlags{
  1990  				AllowIgnoredColumns: false,
  1991  			},
  1992  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  1993  				IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
  1994  					configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
  1995  					configpb.DashboardTabStatusCustomizationOptions_CANCEL,
  1996  				},
  1997  			},
  1998  			expected: summarypb.DashboardTabSummary_FLAKY,
  1999  		},
  2000  		{
  2001  			name:   "neutral statuses ignored with flag enabled (passes ignored)",
  2002  			recent: 4,
  2003  			rows: []*statepb.Row{
  2004  				{
  2005  					Name: "passes and aborts",
  2006  					Results: []int32{
  2007  						int32(statuspb.TestStatus_PASS), 2,
  2008  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2,
  2009  					},
  2010  				},
  2011  				{
  2012  					Name: "passes only",
  2013  					Results: []int32{
  2014  						int32(statuspb.TestStatus_PASS), 4,
  2015  					},
  2016  				},
  2017  			},
  2018  			features: FeatureFlags{
  2019  				AllowIgnoredColumns: true,
  2020  			},
  2021  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2022  				IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
  2023  					configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
  2024  					configpb.DashboardTabStatusCustomizationOptions_CANCEL,
  2025  				},
  2026  			},
  2027  			expected: summarypb.DashboardTabSummary_PASS,
  2028  		},
  2029  		{
  2030  			name:   "neutral statuses ignored with flag enabled (fails detected before ignores)",
  2031  			recent: 4,
  2032  			rows: []*statepb.Row{
  2033  				{
  2034  					Name: "passes and aborts",
  2035  					Results: []int32{
  2036  						int32(statuspb.TestStatus_PASS), 2,
  2037  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 2,
  2038  					},
  2039  				},
  2040  				{
  2041  					Name: "passes and fails",
  2042  					Results: []int32{
  2043  						int32(statuspb.TestStatus_FAIL), 1,
  2044  						int32(statuspb.TestStatus_PASS), 3,
  2045  					},
  2046  				},
  2047  			},
  2048  			features: FeatureFlags{
  2049  				AllowIgnoredColumns: true,
  2050  			},
  2051  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2052  				IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
  2053  					configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
  2054  					configpb.DashboardTabStatusCustomizationOptions_CANCEL,
  2055  				},
  2056  			},
  2057  			expected: summarypb.DashboardTabSummary_FLAKY,
  2058  		},
  2059  	}
  2060  
  2061  	for _, tc := range cases {
  2062  		t.Run(tc.name, func(t *testing.T) {
  2063  			var alerts []*summarypb.FailingTestSummary
  2064  			if tc.alerts {
  2065  				alerts = append(alerts, &summarypb.FailingTestSummary{})
  2066  			}
  2067  
  2068  			if actual := overallStatus(&statepb.Grid{Rows: tc.rows}, tc.recent, tc.stale, tc.broken, alerts, tc.features, tc.colCells, tc.opts); actual != tc.expected {
  2069  				t.Errorf("%s != expected %s", actual, tc.expected)
  2070  			}
  2071  		})
  2072  	}
  2073  }
  2074  
  2075  func makeShim(v ...interface{}) []interface{} {
  2076  	return v
  2077  }
  2078  
  2079  func TestGridMetrics(t *testing.T) {
  2080  	cases := []struct {
  2081  		name            string
  2082  		cols            int
  2083  		rows            []*statepb.Row
  2084  		recent          int
  2085  		features        FeatureFlags
  2086  		opts            *configpb.DashboardTabStatusCustomizationOptions
  2087  		brokenThreshold float32
  2088  		expectedMetrics gridStats
  2089  		expectedBroken  bool
  2090  	}{
  2091  		{
  2092  			name: "no runs",
  2093  		},
  2094  		{
  2095  			name: "what people want (greens)",
  2096  			cols: 2,
  2097  			rows: []*statepb.Row{
  2098  				{
  2099  					Name:    "green eggs",
  2100  					Results: []int32{int32(statuspb.TestStatus_PASS), 2},
  2101  				},
  2102  				{
  2103  					Name:    "and ham",
  2104  					Results: []int32{int32(statuspb.TestStatus_PASS), 2},
  2105  				},
  2106  			},
  2107  			recent: 2,
  2108  			expectedMetrics: gridStats{
  2109  				passingCols:   2,
  2110  				completedCols: 2,
  2111  				passingCells:  4,
  2112  				filledCells:   4,
  2113  			},
  2114  		},
  2115  		{
  2116  			name: "red: i do not like them sam I am",
  2117  			cols: 2,
  2118  			rows: []*statepb.Row{
  2119  				{
  2120  					Name:    "not with a fox",
  2121  					Results: []int32{int32(statuspb.TestStatus_FAIL), 2},
  2122  				},
  2123  				{
  2124  					Name:    "not in a box",
  2125  					Results: []int32{int32(statuspb.TestStatus_FLAKY), 2},
  2126  				},
  2127  			},
  2128  			recent: 2,
  2129  			expectedMetrics: gridStats{
  2130  				passingCols:   0,
  2131  				completedCols: 2,
  2132  				passingCells:  0,
  2133  				filledCells:   4,
  2134  			},
  2135  		},
  2136  		{
  2137  			name: "passing cells but no green columns",
  2138  			cols: 2,
  2139  			rows: []*statepb.Row{
  2140  				{
  2141  					Name: "first doughnut is best",
  2142  					Results: []int32{
  2143  						int32(statuspb.TestStatus_PASS), 1,
  2144  						int32(statuspb.TestStatus_FAIL), 1,
  2145  					},
  2146  				},
  2147  				{
  2148  					Name: "fine wine gets better",
  2149  					Results: []int32{
  2150  						int32(statuspb.TestStatus_FAIL), 1,
  2151  						int32(statuspb.TestStatus_PASS), 1,
  2152  					},
  2153  				},
  2154  			},
  2155  			recent: 2,
  2156  			expectedMetrics: gridStats{
  2157  				passingCols:   0,
  2158  				completedCols: 2,
  2159  				passingCells:  2,
  2160  				filledCells:   4},
  2161  		},
  2162  		{
  2163  			name:   "ignore overflow of claimed columns",
  2164  			cols:   100,
  2165  			recent: 50,
  2166  			rows: []*statepb.Row{
  2167  				{
  2168  					Name:    "a",
  2169  					Results: []int32{int32(statuspb.TestStatus_PASS), 3},
  2170  				},
  2171  				{
  2172  					Name:    "b",
  2173  					Results: []int32{int32(statuspb.TestStatus_PASS), 3},
  2174  				},
  2175  			},
  2176  			expectedMetrics: gridStats{
  2177  				passingCols:   3,
  2178  				completedCols: 3,
  2179  				passingCells:  6,
  2180  				filledCells:   6,
  2181  			},
  2182  		},
  2183  		{
  2184  			name:   "ignore bad row data",
  2185  			cols:   2,
  2186  			recent: 2,
  2187  			rows: []*statepb.Row{
  2188  				{
  2189  					Name: "empty",
  2190  				},
  2191  				{
  2192  					Name:    "filled",
  2193  					Results: []int32{int32(statuspb.TestStatus_PASS), 2},
  2194  				},
  2195  			},
  2196  			expectedMetrics: gridStats{
  2197  				passingCols:   2,
  2198  				completedCols: 2,
  2199  				passingCells:  2,
  2200  				filledCells:   2,
  2201  			},
  2202  		},
  2203  		{
  2204  			name:   "ignore non recent data",
  2205  			cols:   100,
  2206  			recent: 2,
  2207  			rows: []*statepb.Row{
  2208  				{
  2209  					Name:    "data",
  2210  					Results: []int32{int32(statuspb.TestStatus_PASS), 100},
  2211  				},
  2212  			},
  2213  			expectedMetrics: gridStats{
  2214  				passingCols:   2,
  2215  				completedCols: 2,
  2216  				passingCells:  2,
  2217  				filledCells:   2,
  2218  			},
  2219  		},
  2220  		{
  2221  			name:   "no result cells do not alter column",
  2222  			cols:   3,
  2223  			recent: 3,
  2224  			rows: []*statepb.Row{
  2225  				{
  2226  					Name:    "always empty",
  2227  					Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3},
  2228  				},
  2229  				{
  2230  					Name: "first empty",
  2231  					Results: []int32{
  2232  						int32(statuspb.TestStatus_NO_RESULT), 1,
  2233  						int32(statuspb.TestStatus_PASS), 2,
  2234  					},
  2235  				},
  2236  				{
  2237  					Name: "always pass",
  2238  					Results: []int32{
  2239  						int32(statuspb.TestStatus_PASS), 3,
  2240  					},
  2241  				},
  2242  				{
  2243  					Name: "empty, fail, pass",
  2244  					Results: []int32{
  2245  						int32(statuspb.TestStatus_NO_RESULT), 1,
  2246  						int32(statuspb.TestStatus_FAIL), 1,
  2247  						int32(statuspb.TestStatus_PASS), 1,
  2248  					},
  2249  				},
  2250  			},
  2251  			expectedMetrics: gridStats{
  2252  				passingCols:   2, // pass, fail, pass
  2253  				completedCols: 3,
  2254  				passingCells:  6,
  2255  				filledCells:   7,
  2256  			},
  2257  		},
  2258  		{
  2259  			name:   "not enough columns yet works just fine",
  2260  			cols:   4,
  2261  			recent: 50,
  2262  			rows: []*statepb.Row{
  2263  				{
  2264  					Name: "four passes",
  2265  					Results: []int32{
  2266  						int32(statuspb.TestStatus_PASS), 4,
  2267  					},
  2268  				},
  2269  			},
  2270  			expectedMetrics: gridStats{
  2271  				passingCols:   4,
  2272  				completedCols: 4,
  2273  				passingCells:  4,
  2274  				filledCells:   4,
  2275  			},
  2276  		},
  2277  		{
  2278  			name:   "half passes and half fails",
  2279  			cols:   4,
  2280  			recent: 4,
  2281  			rows: []*statepb.Row{
  2282  				{
  2283  					Name: "four passes",
  2284  					Results: []int32{
  2285  						int32(statuspb.TestStatus_PASS), 4,
  2286  					},
  2287  				},
  2288  				{
  2289  					Name: "four fails",
  2290  					Results: []int32{
  2291  						int32(statuspb.TestStatus_FAIL), 4,
  2292  					},
  2293  				},
  2294  			},
  2295  			expectedMetrics: gridStats{
  2296  				passingCols:   0,
  2297  				completedCols: 4,
  2298  				passingCells:  4,
  2299  				filledCells:   8,
  2300  			},
  2301  		},
  2302  		{
  2303  			name:   "no result in every column",
  2304  			cols:   3,
  2305  			recent: 3,
  2306  			rows: []*statepb.Row{
  2307  				{
  2308  					Name:    "always empty",
  2309  					Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3},
  2310  				},
  2311  				{
  2312  					Name: "first empty",
  2313  					Results: []int32{
  2314  						int32(statuspb.TestStatus_NO_RESULT), 1,
  2315  						int32(statuspb.TestStatus_PASS), 2,
  2316  					},
  2317  				},
  2318  				{
  2319  					Name:    "always empty",
  2320  					Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3},
  2321  				},
  2322  			},
  2323  			expectedMetrics: gridStats{
  2324  				passingCols:   2,
  2325  				completedCols: 2,
  2326  				passingCells:  2,
  2327  				filledCells:   2,
  2328  			},
  2329  		},
  2330  		{
  2331  			name:   "only no result",
  2332  			cols:   3,
  2333  			recent: 3,
  2334  			rows: []*statepb.Row{
  2335  				{
  2336  					Name:    "always empty",
  2337  					Results: []int32{int32(statuspb.TestStatus_NO_RESULT), 3},
  2338  				},
  2339  			},
  2340  			expectedMetrics: gridStats{
  2341  				passingCols:   0,
  2342  				completedCols: 0,
  2343  				passingCells:  0,
  2344  				filledCells:   0,
  2345  			},
  2346  		},
  2347  		{
  2348  			name:   "Pass with skips",
  2349  			cols:   3,
  2350  			recent: 3,
  2351  			rows: []*statepb.Row{
  2352  				{
  2353  					Name:    "always empty",
  2354  					Results: []int32{int32(statuspb.TestStatus_PASS_WITH_SKIPS), 3},
  2355  				},
  2356  				{
  2357  					Name:    "all pass",
  2358  					Results: []int32{int32(statuspb.TestStatus_PASS), 3},
  2359  				},
  2360  			},
  2361  			expectedMetrics: gridStats{
  2362  				passingCols:   3,
  2363  				completedCols: 3,
  2364  				passingCells:  6,
  2365  				filledCells:   6,
  2366  			},
  2367  		},
  2368  		{
  2369  			name:   "Pass with errors",
  2370  			cols:   3,
  2371  			recent: 3,
  2372  			rows: []*statepb.Row{
  2373  				{
  2374  					Name:    "always empty",
  2375  					Results: []int32{int32(statuspb.TestStatus_PASS_WITH_ERRORS), 3},
  2376  				},
  2377  				{
  2378  					Name:    "all pass",
  2379  					Results: []int32{int32(statuspb.TestStatus_PASS), 3},
  2380  				},
  2381  			},
  2382  			expectedMetrics: gridStats{
  2383  				passingCols:   3,
  2384  				completedCols: 3,
  2385  				passingCells:  6,
  2386  				filledCells:   6,
  2387  			},
  2388  		},
  2389  		{
  2390  			name:   "All columns past threshold",
  2391  			cols:   4,
  2392  			recent: 4,
  2393  			rows: []*statepb.Row{
  2394  				{
  2395  					Name: "four passes",
  2396  					Results: []int32{
  2397  						int32(statuspb.TestStatus_PASS), 4,
  2398  					},
  2399  				},
  2400  				{
  2401  					Name: "four fails",
  2402  					Results: []int32{
  2403  						int32(statuspb.TestStatus_FAIL), 4,
  2404  					},
  2405  				},
  2406  			},
  2407  			expectedMetrics: gridStats{
  2408  				passingCols:   0,
  2409  				completedCols: 4,
  2410  				passingCells:  4,
  2411  				filledCells:   8,
  2412  			},
  2413  			brokenThreshold: .4,
  2414  			expectedBroken:  true,
  2415  		},
  2416  		{
  2417  			name:   "All columns under threshold",
  2418  			cols:   4,
  2419  			recent: 4,
  2420  			rows: []*statepb.Row{
  2421  				{
  2422  					Name: "four passes",
  2423  					Results: []int32{
  2424  						int32(statuspb.TestStatus_PASS), 4,
  2425  					},
  2426  				},
  2427  				{
  2428  					Name: "four fails",
  2429  					Results: []int32{
  2430  						int32(statuspb.TestStatus_FAIL), 4,
  2431  					},
  2432  				},
  2433  			}, expectedMetrics: gridStats{
  2434  				passingCols:   0,
  2435  				completedCols: 4,
  2436  				passingCells:  4,
  2437  				filledCells:   8,
  2438  			},
  2439  			brokenThreshold: .6,
  2440  			expectedBroken:  false,
  2441  		},
  2442  		{
  2443  			name:   "One column past threshold",
  2444  			cols:   4,
  2445  			recent: 4,
  2446  			rows: []*statepb.Row{
  2447  				{
  2448  					Name: "four passes",
  2449  					Results: []int32{
  2450  						int32(statuspb.TestStatus_PASS), 4,
  2451  					},
  2452  				},
  2453  				{
  2454  					Name: "one pass three fails",
  2455  					Results: []int32{
  2456  						int32(statuspb.TestStatus_FAIL), 1,
  2457  						int32(statuspb.TestStatus_PASS), 3,
  2458  					},
  2459  				},
  2460  			},
  2461  			expectedMetrics: gridStats{
  2462  				passingCols:   3,
  2463  				completedCols: 4,
  2464  				passingCells:  7,
  2465  				filledCells:   8,
  2466  			},
  2467  			brokenThreshold: .4,
  2468  			expectedBroken:  true,
  2469  		},
  2470  		{
  2471  			name:   "One column under threshold",
  2472  			cols:   4,
  2473  			recent: 4,
  2474  			rows: []*statepb.Row{
  2475  				{
  2476  					Name: "four passes",
  2477  					Results: []int32{
  2478  						int32(statuspb.TestStatus_PASS), 4,
  2479  					},
  2480  				},
  2481  				{
  2482  					Name: "one pass three fails",
  2483  					Results: []int32{
  2484  						int32(statuspb.TestStatus_FAIL), 1,
  2485  						int32(statuspb.TestStatus_PASS), 3,
  2486  					},
  2487  				},
  2488  			},
  2489  			expectedMetrics: gridStats{
  2490  				passingCols:   3,
  2491  				completedCols: 4,
  2492  				passingCells:  7,
  2493  				filledCells:   8,
  2494  			},
  2495  			brokenThreshold: .6,
  2496  			expectedBroken:  false,
  2497  		},
  2498  		{
  2499  			name:   "many non-passing/non-failing statuses is not broken",
  2500  			cols:   4,
  2501  			recent: 4,
  2502  			rows: []*statepb.Row{
  2503  				{
  2504  					Name: "four aborts (foo)",
  2505  					Results: []int32{
  2506  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4,
  2507  					},
  2508  				},
  2509  				{
  2510  					Name: "four aborts (bar)",
  2511  					Results: []int32{
  2512  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4,
  2513  					},
  2514  				},
  2515  				{
  2516  					Name: "four aborts (baz)",
  2517  					Results: []int32{
  2518  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4,
  2519  					},
  2520  				},
  2521  			},
  2522  			expectedMetrics: gridStats{
  2523  				passingCols:   0,
  2524  				completedCols: 4,
  2525  				passingCells:  0,
  2526  				filledCells:   12,
  2527  			},
  2528  			brokenThreshold: .6,
  2529  			expectedBroken:  false,
  2530  		},
  2531  		{
  2532  			name:   "many non-passing/non-failing statuses + failing statuses, not broken",
  2533  			cols:   4,
  2534  			recent: 4,
  2535  			rows: []*statepb.Row{
  2536  				{
  2537  					Name: "four aborts (foo)",
  2538  					Results: []int32{
  2539  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4,
  2540  					},
  2541  				},
  2542  				{
  2543  					Name: "four aborts (bar)",
  2544  					Results: []int32{
  2545  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4,
  2546  					},
  2547  				},
  2548  				{
  2549  					Name: "four fails",
  2550  					Results: []int32{
  2551  						int32(statuspb.TestStatus_FAIL), 4,
  2552  					},
  2553  				},
  2554  			},
  2555  			expectedMetrics: gridStats{
  2556  				passingCols:   0,
  2557  				completedCols: 4,
  2558  				passingCells:  0,
  2559  				filledCells:   12,
  2560  			},
  2561  			brokenThreshold: .6,
  2562  			expectedBroken:  false,
  2563  		},
  2564  		{
  2565  			name:   "allow ignored but no ignored test statuses",
  2566  			cols:   4,
  2567  			recent: 4,
  2568  			rows: []*statepb.Row{
  2569  				{
  2570  					Name: "four aborts (foo)",
  2571  					Results: []int32{
  2572  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4,
  2573  					},
  2574  				},
  2575  				{
  2576  					Name: "four aborts (bar)",
  2577  					Results: []int32{
  2578  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 4,
  2579  					},
  2580  				},
  2581  			},
  2582  			features: FeatureFlags{
  2583  				AllowIgnoredColumns: true,
  2584  			},
  2585  			expectedMetrics: gridStats{
  2586  				passingCols:   0,
  2587  				completedCols: 4,
  2588  				passingCells:  0,
  2589  				filledCells:   8,
  2590  				ignoredCols:   0,
  2591  			},
  2592  		},
  2593  		{
  2594  			name:   "allow ignored with ignored test statuses",
  2595  			cols:   4,
  2596  			recent: 4,
  2597  			rows: []*statepb.Row{
  2598  				{
  2599  					Name: "abort with passes",
  2600  					Results: []int32{
  2601  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
  2602  						int32(statuspb.TestStatus_PASS), 3,
  2603  					},
  2604  				},
  2605  				{
  2606  					Name: "unknown with fails",
  2607  					Results: []int32{
  2608  						int32(statuspb.TestStatus_FAIL), 1,
  2609  						int32(statuspb.TestStatus_UNKNOWN), 1,
  2610  						int32(statuspb.TestStatus_FAIL), 2,
  2611  					},
  2612  				},
  2613  			},
  2614  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2615  				IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
  2616  					configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
  2617  					configpb.DashboardTabStatusCustomizationOptions_UNKNOWN,
  2618  				},
  2619  			},
  2620  			features: FeatureFlags{
  2621  				AllowIgnoredColumns: true,
  2622  			},
  2623  			expectedMetrics: gridStats{
  2624  				passingCols:   0,
  2625  				completedCols: 4,
  2626  				passingCells:  3,
  2627  				filledCells:   8,
  2628  				ignoredCols:   2,
  2629  			},
  2630  		},
  2631  		{
  2632  			name:   "do not allow ignored with ignored test statuses",
  2633  			cols:   4,
  2634  			recent: 4,
  2635  			rows: []*statepb.Row{
  2636  				{
  2637  					Name: "abort with passes",
  2638  					Results: []int32{
  2639  						int32(statuspb.TestStatus_CATEGORIZED_ABORT), 1,
  2640  						int32(statuspb.TestStatus_PASS), 3,
  2641  					},
  2642  				},
  2643  				{
  2644  					Name: "unknown with fails",
  2645  					Results: []int32{
  2646  						int32(statuspb.TestStatus_FAIL), 1,
  2647  						int32(statuspb.TestStatus_UNKNOWN), 1,
  2648  						int32(statuspb.TestStatus_PASS), 2,
  2649  					},
  2650  				},
  2651  			},
  2652  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2653  				IgnoredTestStatuses: []configpb.DashboardTabStatusCustomizationOptions_IgnoredTestStatus{
  2654  					configpb.DashboardTabStatusCustomizationOptions_CATEGORIZED_ABORT,
  2655  					configpb.DashboardTabStatusCustomizationOptions_UNKNOWN,
  2656  				},
  2657  			},
  2658  			features: FeatureFlags{
  2659  				AllowIgnoredColumns: false,
  2660  			},
  2661  			expectedMetrics: gridStats{
  2662  				passingCols:   3,
  2663  				completedCols: 4,
  2664  				passingCells:  5,
  2665  				filledCells:   8,
  2666  				ignoredCols:   0,
  2667  			},
  2668  		},
  2669  	}
  2670  
  2671  	for _, tc := range cases {
  2672  		t.Run(tc.name, func(t *testing.T) {
  2673  			if actualMetrics, actualBroken := gridMetrics(tc.cols, tc.rows, tc.recent, tc.brokenThreshold, tc.features, tc.opts); actualMetrics != tc.expectedMetrics || actualBroken != tc.expectedBroken {
  2674  				t.Errorf("%v: gridMetrics() = %v, %v, want %v, %v", tc.name, actualMetrics, actualBroken, tc.expectedMetrics, tc.expectedBroken)
  2675  			}
  2676  		})
  2677  	}
  2678  }
  2679  
  2680  func TestStatusMessage(t *testing.T) {
  2681  	cases := []struct {
  2682  		name     string
  2683  		colCells gridStats
  2684  		status   summarypb.DashboardTabSummary_TabStatus
  2685  		opts     *configpb.DashboardTabStatusCustomizationOptions
  2686  		want     string
  2687  	}{
  2688  		{
  2689  			name: "no filledCells",
  2690  			want: noRuns,
  2691  		},
  2692  		{
  2693  			name: "green path",
  2694  			colCells: gridStats{
  2695  				passingCols:   2,
  2696  				completedCols: 2,
  2697  				passingCells:  4,
  2698  				filledCells:   4,
  2699  			},
  2700  			want: "Tab stats: 2 of 2 (100.0%) recent columns passed (4 of 4 or 100.0% cells)",
  2701  		},
  2702  		{
  2703  			name: "all red path",
  2704  			colCells: gridStats{
  2705  				passingCols:   0,
  2706  				completedCols: 2,
  2707  				passingCells:  0,
  2708  				filledCells:   4,
  2709  			},
  2710  			want: "Tab stats: 0 of 2 (0.0%) recent columns passed (0 of 4 or 0.0% cells)",
  2711  		},
  2712  		{
  2713  			name: "all values the same",
  2714  			colCells: gridStats{
  2715  				passingCols:   2,
  2716  				completedCols: 2,
  2717  				passingCells:  2,
  2718  				filledCells:   2,
  2719  			},
  2720  			want: "Tab stats: 2 of 2 (100.0%) recent columns passed (2 of 2 or 100.0% cells)",
  2721  		},
  2722  		{
  2723  			name: "acceptably flaky without ignored columns",
  2724  			colCells: gridStats{
  2725  				passingCols:   3,
  2726  				completedCols: 4,
  2727  				passingCells:  6,
  2728  				filledCells:   8,
  2729  			},
  2730  			status: summarypb.DashboardTabSummary_ACCEPTABLE,
  2731  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2732  				MaxAcceptableFlakiness: 50,
  2733  			},
  2734  			want: "Tab stats: 3 of 4 (75.0%) recent columns passed (6 of 8 or 75.0% cells)\nStatus info: Recent flakiness (25.0%) over valid columns is within configured acceptable level of 50.0%.",
  2735  		},
  2736  		{
  2737  			name: "acceptably flaky with ignored columns",
  2738  			colCells: gridStats{
  2739  				passingCols:   2,
  2740  				completedCols: 4,
  2741  				ignoredCols:   1,
  2742  				passingCells:  4,
  2743  				filledCells:   8,
  2744  			},
  2745  			status: summarypb.DashboardTabSummary_ACCEPTABLE,
  2746  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2747  				MaxAcceptableFlakiness: 50,
  2748  			},
  2749  			want: "Tab stats: 2 of 4 (50.0%) recent columns passed (4 of 8 or 50.0% cells). 1 columns ignored\nStatus info: Recent flakiness (33.3%) over valid columns is within configured acceptable level of 50.0%.",
  2750  		},
  2751  		{
  2752  			name: "pending tab status without ignored columns",
  2753  			colCells: gridStats{
  2754  				passingCols:   2,
  2755  				completedCols: 3,
  2756  				passingCells:  4,
  2757  				filledCells:   6,
  2758  			},
  2759  			status: summarypb.DashboardTabSummary_PENDING,
  2760  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2761  				MinAcceptableRuns: 4,
  2762  			},
  2763  			want: "Tab stats: 2 of 3 (66.7%) recent columns passed (4 of 6 or 66.7% cells)\nStatus info: Not enough runs",
  2764  		},
  2765  		{
  2766  			name: "pending tab status with ignored columns",
  2767  			colCells: gridStats{
  2768  				passingCols:   2,
  2769  				completedCols: 3,
  2770  				ignoredCols:   1,
  2771  				passingCells:  4,
  2772  				filledCells:   6,
  2773  			},
  2774  			status: summarypb.DashboardTabSummary_PENDING,
  2775  			opts: &configpb.DashboardTabStatusCustomizationOptions{
  2776  				MinAcceptableRuns: 4,
  2777  			},
  2778  			want: "Tab stats: 2 of 3 (66.7%) recent columns passed (4 of 6 or 66.7% cells). 1 columns ignored\nStatus info: Not enough runs",
  2779  		},
  2780  	}
  2781  
  2782  	for _, tc := range cases {
  2783  		t.Run(tc.name, func(t *testing.T) {
  2784  			if actual := statusMessage(tc.colCells, tc.status, tc.opts); actual != tc.want {
  2785  				t.Errorf("%v: statusMessage() = %q, want %q", tc.name, actual, tc.want)
  2786  			}
  2787  		})
  2788  	}
  2789  }
  2790  
  2791  func TestLatestGreen(t *testing.T) {
  2792  	cases := []struct {
  2793  		name     string
  2794  		rows     []*statepb.Row
  2795  		cols     []*statepb.Column
  2796  		expected string
  2797  		first    bool
  2798  	}{
  2799  		{
  2800  			name:     "no recent greens by default",
  2801  			expected: noGreens,
  2802  		},
  2803  		{
  2804  			name:     "no recent greens by default, first green",
  2805  			first:    true,
  2806  			expected: noGreens,
  2807  		},
  2808  		{
  2809  			name: "use build id by default",
  2810  			rows: []*statepb.Row{
  2811  				{
  2812  					Name:    "so pass",
  2813  					Results: []int32{int32(statuspb.TestStatus_PASS), 4},
  2814  				},
  2815  			},
  2816  			cols: []*statepb.Column{
  2817  				{
  2818  					Build: "correct",
  2819  					Extra: []string{"wrong"},
  2820  				},
  2821  			},
  2822  			expected: "correct",
  2823  		},
  2824  		{
  2825  			name: "fall back to build id when headers are missing",
  2826  			rows: []*statepb.Row{
  2827  				{
  2828  					Name:    "so pass",
  2829  					Results: []int32{int32(statuspb.TestStatus_PASS), 4},
  2830  				},
  2831  			},
  2832  			first: true,
  2833  			cols: []*statepb.Column{
  2834  				{
  2835  					Build: "fallback",
  2836  					Extra: []string{},
  2837  				},
  2838  			},
  2839  			expected: "fallback",
  2840  		},
  2841  		{
  2842  			name: "favor first green",
  2843  			rows: []*statepb.Row{
  2844  				{
  2845  					Name:    "so pass",
  2846  					Results: []int32{int32(statuspb.TestStatus_PASS), 4},
  2847  				},
  2848  			},
  2849  			cols: []*statepb.Column{
  2850  				{
  2851  					Extra: []string{"hello", "there"},
  2852  				},
  2853  				{
  2854  					Extra: []string{"bad", "wrong"},
  2855  				},
  2856  			},
  2857  			first:    true,
  2858  			expected: "hello",
  2859  		},
  2860  		{
  2861  			name: "accept any kind of pass",
  2862  			rows: []*statepb.Row{
  2863  				{
  2864  					Name: "pass w/ errors",
  2865  					Results: []int32{
  2866  						int32(statuspb.TestStatus_PASS_WITH_ERRORS), 1,
  2867  						int32(statuspb.TestStatus_PASS), 1,
  2868  					},
  2869  				},
  2870  				{
  2871  					Name:    "pass pass",
  2872  					Results: []int32{int32(statuspb.TestStatus_PASS), 2},
  2873  				},
  2874  				{
  2875  					Name: "pass and skip",
  2876  					Results: []int32{
  2877  						int32(statuspb.TestStatus_PASS_WITH_SKIPS), 1,
  2878  						int32(statuspb.TestStatus_PASS), 1,
  2879  					},
  2880  				},
  2881  			},
  2882  			cols: []*statepb.Column{
  2883  				{
  2884  					Extra: []string{"good"},
  2885  				},
  2886  				{
  2887  					Extra: []string{"bad"},
  2888  				},
  2889  			},
  2890  			first:    true,
  2891  			expected: "good",
  2892  		},
  2893  		{
  2894  			name: "avoid columns with running rows",
  2895  			rows: []*statepb.Row{
  2896  				{
  2897  					Name: "running",
  2898  					Results: []int32{
  2899  						int32(statuspb.TestStatus_RUNNING), 1,
  2900  						int32(statuspb.TestStatus_PASS), 1,
  2901  					},
  2902  				},
  2903  				{
  2904  					Name: "pass",
  2905  					Results: []int32{
  2906  						int32(statuspb.TestStatus_PASS), 2,
  2907  					},
  2908  				},
  2909  			},
  2910  			cols: []*statepb.Column{
  2911  				{
  2912  					Extra: []string{"skip-first-col-still-running"},
  2913  				},
  2914  				{
  2915  					Extra: []string{"accept second-all-finished"},
  2916  				},
  2917  			},
  2918  			first:    true,
  2919  			expected: "accept second-all-finished",
  2920  		},
  2921  		{
  2922  			name: "avoid columns with flakes",
  2923  			rows: []*statepb.Row{
  2924  				{
  2925  					Name: "flaking",
  2926  					Results: []int32{
  2927  						int32(statuspb.TestStatus_FLAKY), 1,
  2928  						int32(statuspb.TestStatus_PASS), 1,
  2929  					},
  2930  				},
  2931  				{
  2932  					Name: "passing",
  2933  					Results: []int32{
  2934  						int32(statuspb.TestStatus_PASS), 2,
  2935  					},
  2936  				},
  2937  			},
  2938  			cols: []*statepb.Column{
  2939  				{
  2940  					Extra: []string{"skip-first-col-with-flake"},
  2941  				},
  2942  				{
  2943  					Extra: []string{"accept second-no-flake"},
  2944  				},
  2945  			},
  2946  			first:    true,
  2947  			expected: "accept second-no-flake",
  2948  		},
  2949  		{
  2950  			name: "avoid columns with failures",
  2951  			rows: []*statepb.Row{
  2952  				{
  2953  					Name: "failing",
  2954  					Results: []int32{
  2955  						int32(statuspb.TestStatus_FAIL), 1,
  2956  						int32(statuspb.TestStatus_PASS), 1,
  2957  					},
  2958  				},
  2959  				{
  2960  					Name: "passing",
  2961  					Results: []int32{
  2962  						int32(statuspb.TestStatus_PASS), 2,
  2963  					},
  2964  				},
  2965  			},
  2966  			cols: []*statepb.Column{
  2967  				{
  2968  					Extra: []string{"skip-first-col-with-fail"},
  2969  				},
  2970  				{
  2971  					Extra: []string{"accept second-after-failure"},
  2972  				},
  2973  			},
  2974  			first:    true,
  2975  			expected: "accept second-after-failure",
  2976  		},
  2977  		{
  2978  			name: "multiple failing columns fixed",
  2979  			rows: []*statepb.Row{
  2980  				{
  2981  					Name: "fail then pass",
  2982  					Results: []int32{
  2983  						int32(statuspb.TestStatus_FAIL), 1,
  2984  						int32(statuspb.TestStatus_PASS), 1,
  2985  					},
  2986  				},
  2987  				{
  2988  					Name: "also fail then pass",
  2989  					Results: []int32{
  2990  						int32(statuspb.TestStatus_FAIL), 1,
  2991  						int32(statuspb.TestStatus_PASS), 2,
  2992  					},
  2993  				},
  2994  			},
  2995  			cols: []*statepb.Column{
  2996  				{
  2997  					Extra: []string{"skip-first-col-with-fail"},
  2998  				},
  2999  				{
  3000  					Extra: []string{"accept second-after-failure"},
  3001  				},
  3002  			},
  3003  			first:    true,
  3004  			expected: "accept second-after-failure",
  3005  		},
  3006  	}
  3007  
  3008  	for _, tc := range cases {
  3009  		t.Run(tc.name, func(t *testing.T) {
  3010  			grid := statepb.Grid{
  3011  				Columns: tc.cols,
  3012  				Rows:    tc.rows,
  3013  			}
  3014  			if actual := latestGreen(&grid, tc.first); actual != tc.expected {
  3015  				t.Errorf("%s != expected %s", actual, tc.expected)
  3016  			}
  3017  		})
  3018  	}
  3019  }
  3020  
  3021  func TestGetHealthinessForInterval(t *testing.T) {
  3022  	now := int64(1000000) // arbitrary time
  3023  	secondsInDay := int64(86400)
  3024  	// These values are *1000 because Column.Started is in milliseconds
  3025  	withinCurrentInterval := (float64(now) - 0.5*float64(secondsInDay)) * 1000.0
  3026  	withinPreviousInterval := (float64(now) - 1.5*float64(secondsInDay)) * 1000.0
  3027  	notWithinAnyInterval := (float64(now) - 3.0*float64(secondsInDay)) * 1000.0
  3028  	cases := []struct {
  3029  		name     string
  3030  		grid     *statepb.Grid
  3031  		tabName  string
  3032  		interval int
  3033  		expected *summarypb.HealthinessInfo
  3034  	}{
  3035  		{
  3036  			name: "typical inputs returns correct HealthinessInfo",
  3037  			grid: &statepb.Grid{
  3038  				Columns: []*statepb.Column{
  3039  					{Started: withinCurrentInterval},
  3040  					{Started: withinCurrentInterval},
  3041  					{Started: withinPreviousInterval},
  3042  					{Started: withinPreviousInterval},
  3043  					{Started: notWithinAnyInterval},
  3044  				},
  3045  				Rows: []*statepb.Row{
  3046  					{
  3047  						Name: "test_1",
  3048  						Results: []int32{
  3049  							statuspb.TestStatus_value["PASS"], 1,
  3050  							statuspb.TestStatus_value["FAIL"], 1,
  3051  							statuspb.TestStatus_value["FAIL"], 1,
  3052  							statuspb.TestStatus_value["FAIL"], 2,
  3053  						},
  3054  						Messages: []string{
  3055  							"",
  3056  							"",
  3057  							"",
  3058  							"infra_fail_1",
  3059  							"",
  3060  						},
  3061  					},
  3062  				},
  3063  			},
  3064  			tabName:  "tab1",
  3065  			interval: 1, // enforce that this equals what secondsInDay is multiplied by below in the Timestamps
  3066  			expected: &summarypb.HealthinessInfo{
  3067  				Start: &timestamp.Timestamp{Seconds: now - secondsInDay},
  3068  				End:   &timestamp.Timestamp{Seconds: now},
  3069  				Tests: []*summarypb.TestInfo{
  3070  					{
  3071  						DisplayName:            "test_1",
  3072  						TotalNonInfraRuns:      2,
  3073  						PassedNonInfraRuns:     1,
  3074  						FailedNonInfraRuns:     1,
  3075  						TotalRunsWithInfra:     2,
  3076  						Flakiness:              50.0,
  3077  						PreviousFlakiness:      []float32{100.0},
  3078  						ChangeFromLastInterval: summarypb.TestInfo_DOWN,
  3079  					},
  3080  				},
  3081  				AverageFlakiness:  50.0,
  3082  				PreviousFlakiness: []float32{100.0},
  3083  			},
  3084  		},
  3085  	}
  3086  
  3087  	for _, tc := range cases {
  3088  		t.Run(tc.name, func(t *testing.T) {
  3089  			if actual := getHealthinessForInterval(tc.grid, tc.tabName, time.Unix(now, 0), tc.interval); !proto.Equal(actual, tc.expected) {
  3090  				t.Errorf("actual: %+v != expected: %+v", actual, tc.expected)
  3091  			}
  3092  		})
  3093  	}
  3094  }
  3095  
  3096  func TestGoBackDays(t *testing.T) {
  3097  	cases := []struct {
  3098  		name        string
  3099  		days        int
  3100  		currentTime time.Time
  3101  		expected    int
  3102  	}{
  3103  		{
  3104  			name:        "0 days returns same Time as input",
  3105  			days:        0,
  3106  			currentTime: time.Unix(0, 0).UTC(),
  3107  			expected:    0,
  3108  		},
  3109  		{
  3110  			name:        "positive days input returns that many days in the past",
  3111  			days:        7,
  3112  			currentTime: time.Unix(0, 0).UTC().AddDate(0, 0, 7), // Gives a date 7 days after Unix 0 time
  3113  			expected:    0,
  3114  		},
  3115  	}
  3116  
  3117  	for _, tc := range cases {
  3118  		t.Run(tc.name, func(t *testing.T) {
  3119  			if actual := goBackDays(tc.days, tc.currentTime); actual != tc.expected {
  3120  				t.Errorf("goBackDays gave actual: %d != expected: %d for days: %d and currentTime: %+v", actual, tc.expected, tc.days, tc.currentTime)
  3121  			}
  3122  		})
  3123  	}
  3124  }
  3125  
  3126  func TestShouldRunHealthiness(t *testing.T) {
  3127  	cases := []struct {
  3128  		name     string
  3129  		tab      *configpb.DashboardTab
  3130  		expected bool
  3131  	}{
  3132  		{
  3133  			name: "tab with false Enable returns false",
  3134  			tab: &configpb.DashboardTab{
  3135  				HealthAnalysisOptions: &configpb.HealthAnalysisOptions{
  3136  					Enable: false,
  3137  				},
  3138  			},
  3139  			expected: false,
  3140  		},
  3141  		{
  3142  			name: "tab with true Enable returns true",
  3143  			tab: &configpb.DashboardTab{
  3144  				HealthAnalysisOptions: &configpb.HealthAnalysisOptions{
  3145  					Enable: true,
  3146  				},
  3147  			},
  3148  			expected: true,
  3149  		},
  3150  		{
  3151  			name:     "tab with nil HealthAnalysisOptions returns false",
  3152  			tab:      &configpb.DashboardTab{},
  3153  			expected: false,
  3154  		},
  3155  	}
  3156  
  3157  	for _, tc := range cases {
  3158  		t.Run(tc.name, func(t *testing.T) {
  3159  			if actual := shouldRunHealthiness(tc.tab); actual != tc.expected {
  3160  				t.Errorf("actual: %t != expected: %t", actual, tc.expected)
  3161  			}
  3162  		})
  3163  	}
  3164  }
  3165  
  3166  func TestCoalesceResult(t *testing.T) {
  3167  	cases := []struct {
  3168  		name     string
  3169  		result   statuspb.TestStatus
  3170  		running  bool
  3171  		expected statuspb.TestStatus
  3172  	}{
  3173  		{
  3174  			name:     "no result by default",
  3175  			expected: statuspb.TestStatus_NO_RESULT,
  3176  		},
  3177  		{
  3178  			name:     "running is no result when ignored",
  3179  			result:   statuspb.TestStatus_RUNNING,
  3180  			expected: statuspb.TestStatus_NO_RESULT,
  3181  			running:  result.IgnoreRunning,
  3182  		},
  3183  		{
  3184  			name:     "running is neutral when shown",
  3185  			result:   statuspb.TestStatus_RUNNING,
  3186  			expected: statuspb.TestStatus_UNKNOWN,
  3187  			running:  result.ShowRunning,
  3188  		},
  3189  		{
  3190  			name:     "fail is fail",
  3191  			result:   statuspb.TestStatus_FAIL,
  3192  			expected: statuspb.TestStatus_FAIL,
  3193  		},
  3194  		{
  3195  			name:     "flaky is flaky",
  3196  			result:   statuspb.TestStatus_FLAKY,
  3197  			expected: statuspb.TestStatus_FLAKY,
  3198  		},
  3199  		{
  3200  			name:     "simplify pass",
  3201  			result:   statuspb.TestStatus_PASS_WITH_ERRORS,
  3202  			expected: statuspb.TestStatus_PASS,
  3203  		},
  3204  		{
  3205  			name:     "categorized abort is neutral",
  3206  			result:   statuspb.TestStatus_CATEGORIZED_ABORT,
  3207  			expected: statuspb.TestStatus_UNKNOWN,
  3208  		},
  3209  	}
  3210  
  3211  	for _, tc := range cases {
  3212  		t.Run(tc.name, func(t *testing.T) {
  3213  			if actual := coalesceResult(tc.result, tc.running); actual != tc.expected {
  3214  				t.Errorf("actual %s != expected %s", actual, tc.expected)
  3215  			}
  3216  		})
  3217  	}
  3218  }
  3219  
  3220  func TestResultIter(t *testing.T) {
  3221  	cases := []struct {
  3222  		name     string
  3223  		cancel   int
  3224  		in       []int32
  3225  		expected []statuspb.TestStatus
  3226  	}{
  3227  		{
  3228  			name: "basically works",
  3229  			in: []int32{
  3230  				int32(statuspb.TestStatus_PASS), 3,
  3231  				int32(statuspb.TestStatus_FAIL), 2,
  3232  			},
  3233  			expected: []statuspb.TestStatus{
  3234  				statuspb.TestStatus_PASS,
  3235  				statuspb.TestStatus_PASS,
  3236  				statuspb.TestStatus_PASS,
  3237  				statuspb.TestStatus_FAIL,
  3238  				statuspb.TestStatus_FAIL,
  3239  			},
  3240  		},
  3241  		{
  3242  			name: "ignore last unbalanced input",
  3243  			in: []int32{
  3244  				int32(statuspb.TestStatus_PASS), 3,
  3245  				int32(statuspb.TestStatus_FAIL),
  3246  			},
  3247  			expected: []statuspb.TestStatus{
  3248  				statuspb.TestStatus_PASS,
  3249  				statuspb.TestStatus_PASS,
  3250  				statuspb.TestStatus_PASS,
  3251  			},
  3252  		},
  3253  	}
  3254  
  3255  	for _, tc := range cases {
  3256  		t.Run(tc.name, func(t *testing.T) {
  3257  			iter := result.Iter(tc.in)
  3258  			var actual []statuspb.TestStatus
  3259  			var idx int
  3260  			for {
  3261  				val, ok := iter()
  3262  				if !ok {
  3263  					return
  3264  				}
  3265  				idx++
  3266  				actual = append(actual, val)
  3267  			}
  3268  			if !cmp.Equal(actual, tc.expected, protocmp.Transform()) {
  3269  				t.Errorf("%s != expected %s", actual, tc.expected)
  3270  			}
  3271  		})
  3272  	}
  3273  }
  3274  
  3275  func TestSummaryPath(t *testing.T) {
  3276  	mustPath := func(s string) *gcs.Path {
  3277  		p, err := gcs.NewPath(s)
  3278  		if err != nil {
  3279  			t.Fatalf("gcs.NewPath(%q) got err: %v", s, err)
  3280  		}
  3281  		return p
  3282  	}
  3283  	cases := []struct {
  3284  		name   string
  3285  		path   gcs.Path
  3286  		prefix string
  3287  		dash   string
  3288  		want   *gcs.Path
  3289  		err    bool
  3290  	}{
  3291  		{
  3292  			name: "normal",
  3293  			path: *mustPath("gs://bucket/config"),
  3294  			dash: "hello",
  3295  			want: mustPath("gs://bucket/summary-hello"),
  3296  		},
  3297  		{
  3298  			name:   "prefix", // construct path with a prefix correctly
  3299  			path:   *mustPath("gs://bucket/config"),
  3300  			prefix: "summary",
  3301  			dash:   "hello",
  3302  			want:   mustPath("gs://bucket/summary/summary-hello"),
  3303  		},
  3304  		{
  3305  			name:   "normalize", // normalize dashboard name correctly
  3306  			path:   *mustPath("gs://bucket/config"),
  3307  			prefix: "UpperCase",       // do not normalize
  3308  			dash:   "Hello --- World", // normalize
  3309  			want:   mustPath("gs://bucket/UpperCase/summary-helloworld"),
  3310  		},
  3311  	}
  3312  
  3313  	for _, tc := range cases {
  3314  		t.Run(tc.name, func(t *testing.T) {
  3315  			got, err := SummaryPath(tc.path, tc.prefix, tc.dash)
  3316  			switch {
  3317  			case err != nil:
  3318  				if !tc.err {
  3319  					t.Errorf("summaryPath(%q, %q, %q) got unexpected error: %v", tc.path, tc.prefix, tc.dash, err)
  3320  				}
  3321  			case tc.err:
  3322  				t.Errorf("summaryPath(%q, %q, %q) failed to get an error", tc.path, tc.prefix, tc.name)
  3323  			default:
  3324  				if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(gcs.Path{})); diff != "" {
  3325  					t.Errorf("summaryPath(%q, %q, %q) got unexpected diff (-want +got):\n%s", tc.path, tc.prefix, tc.dash, diff)
  3326  				}
  3327  			}
  3328  		})
  3329  	}
  3330  }
  3331  
  3332  func TestTestResultLink(t *testing.T) {
  3333  	cases := []struct {
  3334  		name                   string
  3335  		template               *configpb.LinkTemplate
  3336  		properties             map[string]string
  3337  		testID                 string
  3338  		target                 string
  3339  		buildID                string
  3340  		gcsPrefix              string
  3341  		propertyToColumnHeader map[string]string
  3342  		customColumnHeaders    map[string]string
  3343  		want                   string
  3344  	}{
  3345  		{
  3346  			name: "nil",
  3347  			want: "",
  3348  		},
  3349  		{
  3350  			name: "empty",
  3351  			template: &configpb.LinkTemplate{
  3352  				Url: "https://test.com/<encode:<workflow-name>>/<workflow-id>/<test-id>/<encode:<test-name>>",
  3353  				Options: []*configpb.LinkOptionsTemplate{
  3354  					{
  3355  						Key:   "prefix",
  3356  						Value: "<gcs-prefix>",
  3357  					},
  3358  					{
  3359  						Key:   "build",
  3360  						Value: "<build-id>",
  3361  					},
  3362  					{
  3363  						Key:   "prop",
  3364  						Value: "<my-prop>",
  3365  					},
  3366  					{
  3367  						Key:   "foo",
  3368  						Value: "<custom-0>",
  3369  					},
  3370  				},
  3371  			},
  3372  			properties:             map[string]string{},
  3373  			testID:                 "",
  3374  			target:                 "",
  3375  			buildID:                "",
  3376  			gcsPrefix:              "",
  3377  			propertyToColumnHeader: map[string]string{},
  3378  			customColumnHeaders:    map[string]string{},
  3379  			want:                   "https://test.com/%3Cencode:%3Cworkflow-name%3E%3E/%3Cworkflow-id%3E//?build=&foo=%3Ccustom-0%3E&prefix=&prop=%3Cmy-prop%3E",
  3380  		},
  3381  		{
  3382  			name:     "empty template",
  3383  			template: &configpb.LinkTemplate{},
  3384  			properties: map[string]string{
  3385  				"workflow-id":   "workflow-id-1",
  3386  				"workflow-name": "//my:workflow",
  3387  				"my-prop":       "foo",
  3388  			},
  3389  			testID:    "my-test-id-1",
  3390  			target:    "//path/to:my-test",
  3391  			buildID:   "build-1",
  3392  			gcsPrefix: "my-bucket/has/results",
  3393  			propertyToColumnHeader: map[string]string{
  3394  				"<custom-0>": "apple",
  3395  			},
  3396  			customColumnHeaders: map[string]string{
  3397  				"apple": "fruit",
  3398  			},
  3399  			want: "",
  3400  		},
  3401  		{
  3402  			name: "basically works",
  3403  			template: &configpb.LinkTemplate{
  3404  				Url: "https://test.com/<encode:<workflow-name>>/<workflow-id>/<test-id>/<encode:<test-name>>",
  3405  				Options: []*configpb.LinkOptionsTemplate{
  3406  					{
  3407  						Key:   "prefix",
  3408  						Value: "<gcs-prefix>",
  3409  					},
  3410  					{
  3411  						Key:   "build",
  3412  						Value: "<build-id>",
  3413  					},
  3414  					{
  3415  						Key:   "prop",
  3416  						Value: "<my-prop>",
  3417  					},
  3418  					{
  3419  						Key:   "foo",
  3420  						Value: "<custom-0>",
  3421  					},
  3422  					{
  3423  						Key:   "hello",
  3424  						Value: "<custom-1>",
  3425  					},
  3426  				},
  3427  			},
  3428  			properties: map[string]string{
  3429  				"workflow-id":   "workflow-id-1",
  3430  				"workflow-name": "//my:workflow",
  3431  				"my-prop":       "foo",
  3432  			},
  3433  			testID:    "my-test-id-1",
  3434  			target:    "//path/to:my-test",
  3435  			buildID:   "build-1",
  3436  			gcsPrefix: "my-bucket/has/results",
  3437  			propertyToColumnHeader: map[string]string{
  3438  				"<custom-0>": "foo",
  3439  				"<custom-1>": "hello",
  3440  			},
  3441  			customColumnHeaders: map[string]string{
  3442  				"foo":   "bar",
  3443  				"hello": "world",
  3444  			},
  3445  			want: "https://test.com/%2F%2Fmy:workflow/workflow-id-1/my-test-id-1/%2F%2Fpath%2Fto:my-test?build=build-1&foo=bar&hello=world&prefix=my-bucket%2Fhas%2Fresults&prop=foo",
  3446  		},
  3447  		{
  3448  			name: "non-matching tokens",
  3449  			template: &configpb.LinkTemplate{
  3450  				Url: "https://test.com/<greeting>",
  3451  				Options: []*configpb.LinkOptionsTemplate{
  3452  					{
  3453  						Key:   "farewell",
  3454  						Value: "<farewell>",
  3455  					},
  3456  					{
  3457  						Key:   "bye",
  3458  						Value: "<custom-0>",
  3459  					},
  3460  				},
  3461  			},
  3462  			properties: map[string]string{
  3463  				"workflow-id":   "workflow-id-1",
  3464  				"workflow-name": "//my:workflow",
  3465  				"my-prop":       "foo",
  3466  			},
  3467  			propertyToColumnHeader: map[string]string{
  3468  				"<custom-0>": "bye",
  3469  			},
  3470  			customColumnHeaders: map[string]string{
  3471  				"foo": "bar",
  3472  			},
  3473  			testID:    "my-test-id-1",
  3474  			target:    "//path/to:my-test",
  3475  			buildID:   "build-1",
  3476  			gcsPrefix: "my-bucket/has/results",
  3477  			want:      "https://test.com/%3Cgreeting%3E?bye=%3Ccustom-0%3E&farewell=%3Cfarewell%3E",
  3478  		},
  3479  		{
  3480  			name: "basically works, nil properties",
  3481  			template: &configpb.LinkTemplate{
  3482  				Url: "https://test.com/<encode:<workflow-name>>/<workflow-id>/<test-id>/<encode:<test-name>>",
  3483  				Options: []*configpb.LinkOptionsTemplate{
  3484  					{
  3485  						Key:   "prefix",
  3486  						Value: "<gcs-prefix>",
  3487  					},
  3488  					{
  3489  						Key:   "build",
  3490  						Value: "<build-id>",
  3491  					},
  3492  				},
  3493  			},
  3494  			properties: nil,
  3495  			testID:     "my-test-id-1",
  3496  			target:     "//path/to:my-test",
  3497  			buildID:    "build-1",
  3498  			gcsPrefix:  "my-bucket/has/results",
  3499  			want:       "https://test.com/%3Cencode:%3Cworkflow-name%3E%3E/%3Cworkflow-id%3E/my-test-id-1///path/to:my-test?build=build-1&prefix=my-bucket%2Fhas%2Fresults",
  3500  		},
  3501  	}
  3502  
  3503  	for _, tc := range cases {
  3504  		t.Run(tc.name, func(t *testing.T) {
  3505  			if got := testResultLink(tc.template, tc.properties, tc.testID, tc.target, tc.buildID, tc.gcsPrefix, tc.propertyToColumnHeader, tc.customColumnHeaders); got != tc.want {
  3506  				t.Errorf("testResultLink(%v, %v, %s, %s, %s, %s, %s, %s) = %q, want %q", tc.template, tc.properties, tc.testID, tc.target, tc.buildID, tc.gcsPrefix, tc.propertyToColumnHeader, tc.customColumnHeaders, got, tc.want)
  3507  			}
  3508  		})
  3509  	}
  3510  }