github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/tabulator/tabstate_test.go (about)

     1  /*
     2  Copyright 2022 The TestGrid Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package tabulator
    18  
    19  import (
    20  	"context"
    21  	"testing"
    22  
    23  	"github.com/google/go-cmp/cmp"
    24  	"github.com/sirupsen/logrus"
    25  	"google.golang.org/protobuf/testing/protocmp"
    26  
    27  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    28  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    29  	tspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    30  	"github.com/GoogleCloudPlatform/testgrid/pkg/updater"
    31  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    32  	"github.com/GoogleCloudPlatform/testgrid/util/gcs/fake"
    33  )
    34  
    35  func TestTabStatePath(t *testing.T) {
    36  	path := newPathOrDie("gs://bucket/config")
    37  	cases := []struct {
    38  		name           string
    39  		dashboardName  string
    40  		tabName        string
    41  		tabStatePrefix string
    42  		expected       *gcs.Path
    43  	}{
    44  		{
    45  			name:     "basically works",
    46  			expected: path,
    47  		},
    48  		{
    49  			name:          "invalid dashboard name errors",
    50  			dashboardName: "---://foo",
    51  			tabName:       "ok",
    52  		},
    53  		{
    54  			name:          "invalid tab name errors",
    55  			dashboardName: "cool",
    56  			tabName:       "--??!f///",
    57  		},
    58  		{
    59  			name:          "bucket change errors",
    60  			dashboardName: "gs://honey-bucket/config",
    61  			tabName:       "tab",
    62  		},
    63  		{
    64  			name:          "normal behavior works",
    65  			dashboardName: "dashboard",
    66  			tabName:       "some-tab",
    67  			expected:      newPathOrDie("gs://bucket/dashboard/some-tab"),
    68  		},
    69  		{
    70  			name:           "target a subfolder works",
    71  			tabStatePrefix: "tab-state",
    72  			dashboardName:  "dashboard",
    73  			tabName:        "some-tab",
    74  			expected:       newPathOrDie("gs://bucket/tab-state/dashboard/some-tab"),
    75  		},
    76  	}
    77  
    78  	for _, tc := range cases {
    79  		t.Run(tc.name, func(t *testing.T) {
    80  			actual, err := TabStatePath(*path, tc.tabStatePrefix, tc.dashboardName, tc.tabName)
    81  			switch {
    82  			case err != nil:
    83  				if tc.expected != nil {
    84  					t.Errorf("tabStatePath(%v, %v) got unexpected error: %v", tc.dashboardName, tc.tabName, err)
    85  				}
    86  			case tc.expected == nil:
    87  				t.Errorf("tabStatePath(%v, %v) failed to receive an error", tc.dashboardName, tc.tabName)
    88  			default:
    89  				if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcs.Path{})); diff != "" {
    90  					t.Errorf("tabStatePath(%v, %v) got unexpected diff (-have, +want):\n%s", tc.dashboardName, tc.tabName, diff)
    91  				}
    92  			}
    93  		})
    94  	}
    95  }
    96  
    97  func newPathOrDie(s string) *gcs.Path {
    98  	p, err := gcs.NewPath(s)
    99  	if err != nil {
   100  		panic(err)
   101  	}
   102  	return p
   103  }
   104  
   105  func Test_DropEmptyColumns(t *testing.T) {
   106  	testcases := []struct {
   107  		name     string
   108  		grid     []updater.InflatedColumn
   109  		expected []updater.InflatedColumn
   110  	}{
   111  		{
   112  			name:     "empty",
   113  			grid:     []updater.InflatedColumn{},
   114  			expected: []updater.InflatedColumn{},
   115  		},
   116  		{
   117  			name:     "nil",
   118  			grid:     nil,
   119  			expected: []updater.InflatedColumn{},
   120  		},
   121  		{
   122  			name: "drops empty column",
   123  			grid: []updater.InflatedColumn{
   124  				{
   125  					Column: &statepb.Column{Name: "full"},
   126  					Cells: map[string]updater.Cell{
   127  						"first":  {Result: tspb.TestStatus_BUILD_PASSED},
   128  						"second": {Result: tspb.TestStatus_PASS_WITH_SKIPS},
   129  						"third":  {Result: tspb.TestStatus_FAIL},
   130  					},
   131  				},
   132  				{
   133  					Column: &statepb.Column{Name: "empty"},
   134  					Cells: map[string]updater.Cell{
   135  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   136  						"second": {Result: tspb.TestStatus_NO_RESULT},
   137  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   138  					},
   139  				},
   140  				{
   141  					Column: &statepb.Column{Name: "sparse"},
   142  					Cells: map[string]updater.Cell{
   143  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   144  						"second": {Result: tspb.TestStatus_TIMED_OUT},
   145  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   146  					},
   147  				},
   148  			},
   149  			expected: []updater.InflatedColumn{
   150  				{
   151  					Column: &statepb.Column{Name: "full"},
   152  					Cells: map[string]updater.Cell{
   153  						"first":  {Result: tspb.TestStatus_BUILD_PASSED},
   154  						"second": {Result: tspb.TestStatus_PASS_WITH_SKIPS},
   155  						"third":  {Result: tspb.TestStatus_FAIL},
   156  					},
   157  				},
   158  				{
   159  					Column: &statepb.Column{Name: "sparse"},
   160  					Cells: map[string]updater.Cell{
   161  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   162  						"second": {Result: tspb.TestStatus_TIMED_OUT},
   163  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   164  					},
   165  				},
   166  			},
   167  		},
   168  		{
   169  			name: "drop multiple columns in a row",
   170  			grid: []updater.InflatedColumn{
   171  				{
   172  					Column: &statepb.Column{Name: "empty"},
   173  					Cells: map[string]updater.Cell{
   174  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   175  						"second": {Result: tspb.TestStatus_NO_RESULT},
   176  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   177  					},
   178  				},
   179  				{
   180  					Column: &statepb.Column{Name: "nothing"},
   181  					Cells: map[string]updater.Cell{
   182  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   183  						"second": {Result: tspb.TestStatus_NO_RESULT},
   184  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   185  					},
   186  				},
   187  				{
   188  					Column: &statepb.Column{Name: "full"},
   189  					Cells: map[string]updater.Cell{
   190  						"first":  {Result: tspb.TestStatus_BUILD_PASSED},
   191  						"second": {Result: tspb.TestStatus_PASS_WITH_SKIPS},
   192  						"third":  {Result: tspb.TestStatus_FAIL},
   193  					},
   194  				},
   195  				{
   196  					Column: &statepb.Column{Name: "zero"},
   197  					Cells: map[string]updater.Cell{
   198  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   199  						"second": {Result: tspb.TestStatus_NO_RESULT},
   200  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   201  					},
   202  				},
   203  				{
   204  					Column: &statepb.Column{Name: "nada"},
   205  					Cells: map[string]updater.Cell{
   206  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   207  						"second": {Result: tspb.TestStatus_NO_RESULT},
   208  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   209  					},
   210  				},
   211  			},
   212  			expected: []updater.InflatedColumn{
   213  				{
   214  					Column: &statepb.Column{Name: "full"},
   215  					Cells: map[string]updater.Cell{
   216  						"first":  {Result: tspb.TestStatus_BUILD_PASSED},
   217  						"second": {Result: tspb.TestStatus_PASS_WITH_SKIPS},
   218  						"third":  {Result: tspb.TestStatus_FAIL},
   219  					},
   220  				},
   221  			},
   222  		},
   223  		{
   224  			name: "don't drop everything",
   225  			grid: []updater.InflatedColumn{
   226  				{
   227  					Column: &statepb.Column{Name: "first"},
   228  					Cells: map[string]updater.Cell{
   229  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   230  						"second": {Result: tspb.TestStatus_NO_RESULT},
   231  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   232  					},
   233  				},
   234  				{
   235  					Column: &statepb.Column{Name: "empty"},
   236  					Cells: map[string]updater.Cell{
   237  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   238  						"second": {Result: tspb.TestStatus_NO_RESULT},
   239  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   240  					},
   241  				},
   242  				{
   243  					Column: &statepb.Column{Name: "nada"},
   244  					Cells: map[string]updater.Cell{
   245  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   246  						"second": {Result: tspb.TestStatus_NO_RESULT},
   247  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   248  					},
   249  				},
   250  			},
   251  			expected: []updater.InflatedColumn{
   252  				{
   253  					Column: &statepb.Column{Name: "first"},
   254  					Cells: map[string]updater.Cell{
   255  						"first":  {Result: tspb.TestStatus_NO_RESULT},
   256  						"second": {Result: tspb.TestStatus_NO_RESULT},
   257  						"third":  {Result: tspb.TestStatus_NO_RESULT},
   258  					},
   259  				},
   260  			},
   261  		},
   262  	}
   263  
   264  	for _, tc := range testcases {
   265  		t.Run(tc.name, func(t *testing.T) {
   266  			actual := dropEmptyColumns(tc.grid)
   267  			if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" {
   268  				t.Errorf("(-got, +want): %s", diff)
   269  			}
   270  		})
   271  	}
   272  }
   273  
   274  func Test_Tabulate(t *testing.T) {
   275  	testcases := []struct {
   276  		name           string
   277  		grid           *statepb.Grid
   278  		dashCfg        *configpb.DashboardTab
   279  		groupCfg       *configpb.TestGroup
   280  		calculateStats bool
   281  		useTabAlert    bool
   282  		expected       *statepb.Grid
   283  	}{
   284  		{
   285  			name:     "empty grid is tolerated",
   286  			grid:     &statepb.Grid{},
   287  			dashCfg:  &configpb.DashboardTab{},
   288  			groupCfg: &configpb.TestGroup{},
   289  			expected: &statepb.Grid{},
   290  		},
   291  		{
   292  			name:     "nil grid is not tolerated",
   293  			dashCfg:  &configpb.DashboardTab{},
   294  			groupCfg: &configpb.TestGroup{},
   295  		},
   296  		{
   297  			name: "nil config is not tolerated",
   298  			grid: &statepb.Grid{},
   299  		},
   300  		{
   301  			name: "basic grid",
   302  			grid: buildGrid(t,
   303  				updater.InflatedColumn{
   304  					Column: &statepb.Column{Name: "okay"},
   305  					Cells: map[string]updater.Cell{
   306  						"first":  {Result: tspb.TestStatus_BUILD_PASSED},
   307  						"second": {Result: tspb.TestStatus_BUILD_PASSED},
   308  					},
   309  				},
   310  				updater.InflatedColumn{
   311  					Column: &statepb.Column{Name: "still-ok"},
   312  					Cells: map[string]updater.Cell{
   313  						"first":  {Result: tspb.TestStatus_BUILD_PASSED},
   314  						"second": {Result: tspb.TestStatus_BUILD_PASSED},
   315  					},
   316  				}),
   317  			dashCfg: &configpb.DashboardTab{
   318  				Name: "tab",
   319  			},
   320  			groupCfg: &configpb.TestGroup{},
   321  			expected: &statepb.Grid{
   322  				Columns: []*statepb.Column{
   323  					{Name: "okay"},
   324  					{Name: "still-ok"},
   325  				},
   326  				Rows: []*statepb.Row{
   327  					{
   328  						Name:    "first",
   329  						Id:      "first",
   330  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2},
   331  					},
   332  					{
   333  						Name:    "second",
   334  						Id:      "second",
   335  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2},
   336  					},
   337  				},
   338  			},
   339  		},
   340  		{
   341  			name: "Filters out regex tabs",
   342  			grid: buildGrid(t,
   343  				updater.InflatedColumn{
   344  					Column: &statepb.Column{Name: "okay"},
   345  					Cells: map[string]updater.Cell{
   346  						"first": {Result: tspb.TestStatus_BUILD_PASSED},
   347  						"bad":   {Result: tspb.TestStatus_BUILD_PASSED},
   348  					},
   349  				},
   350  				updater.InflatedColumn{
   351  					Column: &statepb.Column{Name: "still-ok"},
   352  					Cells: map[string]updater.Cell{
   353  						"first": {Result: tspb.TestStatus_BUILD_PASSED},
   354  						"bad":   {Result: tspb.TestStatus_BUILD_PASSED},
   355  					},
   356  				}),
   357  			dashCfg: &configpb.DashboardTab{
   358  				Name:        "tab",
   359  				BaseOptions: "exclude-filter-by-regex=bad",
   360  			},
   361  			groupCfg: &configpb.TestGroup{},
   362  			expected: &statepb.Grid{
   363  				Columns: []*statepb.Column{
   364  					{Name: "okay"},
   365  					{Name: "still-ok"},
   366  				},
   367  				Rows: []*statepb.Row{
   368  					{
   369  						Name:    "first",
   370  						Id:      "first",
   371  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2},
   372  					},
   373  				},
   374  			},
   375  		},
   376  		{
   377  			name: "Filters out regex tabs, and drops empty columns",
   378  			grid: buildGrid(t,
   379  				updater.InflatedColumn{
   380  					Column: &statepb.Column{Name: "okay"},
   381  					Cells: map[string]updater.Cell{
   382  						"first": {Result: tspb.TestStatus_BUILD_PASSED},
   383  					},
   384  				},
   385  				updater.InflatedColumn{
   386  					Column: &statepb.Column{Name: "weird"},
   387  					Cells: map[string]updater.Cell{
   388  						"bad": {Result: tspb.TestStatus_BUILD_PASSED},
   389  					},
   390  				},
   391  				updater.InflatedColumn{
   392  					Column: &statepb.Column{Name: "still-ok"},
   393  					Cells: map[string]updater.Cell{
   394  						"first": {Result: tspb.TestStatus_BUILD_PASSED},
   395  					},
   396  				}),
   397  			dashCfg: &configpb.DashboardTab{
   398  				Name:        "tab",
   399  				BaseOptions: "exclude-filter-by-regex=bad",
   400  			},
   401  			groupCfg: &configpb.TestGroup{},
   402  			expected: &statepb.Grid{
   403  				Columns: []*statepb.Column{
   404  					{Name: "okay"},
   405  					{Name: "still-ok"},
   406  				},
   407  				Rows: []*statepb.Row{
   408  					{
   409  						Name:    "first",
   410  						Id:      "first",
   411  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2},
   412  					},
   413  				},
   414  			},
   415  		},
   416  		{
   417  			name: "calculate alerts using test group",
   418  			grid: buildGrid(t,
   419  				updater.InflatedColumn{
   420  					Column: &statepb.Column{Name: "final"},
   421  					Cells: map[string]updater.Cell{
   422  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   423  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   424  						"flaky":  {Result: tspb.TestStatus_BUILD_PASSED},
   425  					},
   426  				},
   427  				updater.InflatedColumn{
   428  					Column: &statepb.Column{Name: "middle"},
   429  					Cells: map[string]updater.Cell{
   430  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   431  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   432  						"flaky":  {Result: tspb.TestStatus_BUILD_FAIL},
   433  					},
   434  				},
   435  				updater.InflatedColumn{
   436  					Column: &statepb.Column{Name: "initial"},
   437  					Cells: map[string]updater.Cell{
   438  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   439  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   440  						"flaky":  {Result: tspb.TestStatus_BUILD_PASSED},
   441  					},
   442  				}),
   443  			dashCfg: &configpb.DashboardTab{},
   444  			groupCfg: &configpb.TestGroup{
   445  				Name:                    "group",
   446  				NumFailuresToAlert:      1,
   447  				NumPassesToDisableAlert: 1,
   448  			},
   449  			useTabAlert: false,
   450  			expected: &statepb.Grid{
   451  				Columns: []*statepb.Column{
   452  					{Name: "final"},
   453  					{Name: "middle"},
   454  					{Name: "initial"},
   455  				},
   456  				Rows: []*statepb.Row{
   457  					{
   458  						Name:    "okay",
   459  						Id:      "okay",
   460  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 3},
   461  					},
   462  					{
   463  						Name:    "broken",
   464  						Id:      "broken",
   465  						Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 3},
   466  						AlertInfo: &statepb.AlertInfo{
   467  							FailCount: 3,
   468  						},
   469  					},
   470  					{
   471  						Name:    "flaky",
   472  						Id:      "flaky",
   473  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1, int32(tspb.TestStatus_BUILD_FAIL), 1, int32(tspb.TestStatus_BUILD_PASSED), 1},
   474  					},
   475  				},
   476  			},
   477  		},
   478  		{
   479  			name: "calculate alerts using tab state",
   480  			grid: buildGrid(t,
   481  				updater.InflatedColumn{
   482  					Column: &statepb.Column{Name: "final"},
   483  					Cells: map[string]updater.Cell{
   484  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   485  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   486  						"flaky":  {Result: tspb.TestStatus_BUILD_PASSED},
   487  					},
   488  				},
   489  				updater.InflatedColumn{
   490  					Column: &statepb.Column{Name: "middle"},
   491  					Cells: map[string]updater.Cell{
   492  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   493  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   494  						"flaky":  {Result: tspb.TestStatus_BUILD_FAIL},
   495  					},
   496  				},
   497  				updater.InflatedColumn{
   498  					Column: &statepb.Column{Name: "initial"},
   499  					Cells: map[string]updater.Cell{
   500  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   501  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   502  						"flaky":  {Result: tspb.TestStatus_BUILD_PASSED},
   503  					},
   504  				}),
   505  			dashCfg: &configpb.DashboardTab{
   506  				Name: "tab",
   507  				AlertOptions: &configpb.DashboardTabAlertOptions{
   508  					NumFailuresToAlert:      1,
   509  					NumPassesToDisableAlert: 1,
   510  				},
   511  			},
   512  			groupCfg:    &configpb.TestGroup{},
   513  			useTabAlert: true,
   514  			expected: &statepb.Grid{
   515  				Columns: []*statepb.Column{
   516  					{Name: "final"},
   517  					{Name: "middle"},
   518  					{Name: "initial"},
   519  				},
   520  				Rows: []*statepb.Row{
   521  					{
   522  						Name:    "okay",
   523  						Id:      "okay",
   524  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 3},
   525  					},
   526  					{
   527  						Name:    "broken",
   528  						Id:      "broken",
   529  						Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 3},
   530  						AlertInfo: &statepb.AlertInfo{
   531  							FailCount: 3,
   532  						},
   533  					},
   534  					{
   535  						Name:    "flaky",
   536  						Id:      "flaky",
   537  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1, int32(tspb.TestStatus_BUILD_FAIL), 1, int32(tspb.TestStatus_BUILD_PASSED), 1},
   538  					},
   539  				},
   540  			},
   541  		},
   542  		{
   543  			name: "calculate stats",
   544  			grid: buildGrid(t,
   545  				updater.InflatedColumn{
   546  					Column: &statepb.Column{Name: "final"},
   547  					Cells: map[string]updater.Cell{
   548  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   549  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   550  						"flaky":  {Result: tspb.TestStatus_BUILD_PASSED},
   551  					},
   552  				},
   553  				updater.InflatedColumn{
   554  					Column: &statepb.Column{Name: "middle"},
   555  					Cells: map[string]updater.Cell{
   556  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   557  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   558  						"flaky":  {Result: tspb.TestStatus_BUILD_FAIL},
   559  					},
   560  				},
   561  				updater.InflatedColumn{
   562  					Column: &statepb.Column{Name: "initial"},
   563  					Cells: map[string]updater.Cell{
   564  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   565  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   566  						"flaky":  {Result: tspb.TestStatus_BUILD_PASSED},
   567  					},
   568  				}),
   569  			dashCfg: &configpb.DashboardTab{
   570  				Name: "tab",
   571  				AlertOptions: &configpb.DashboardTabAlertOptions{
   572  					NumFailuresToAlert:      1,
   573  					NumPassesToDisableAlert: 1,
   574  				},
   575  				BrokenColumnThreshold: 0.5,
   576  			},
   577  			groupCfg:       &configpb.TestGroup{},
   578  			useTabAlert:    true,
   579  			calculateStats: true,
   580  			expected: &statepb.Grid{
   581  				Columns: []*statepb.Column{
   582  					{
   583  						Name: "final",
   584  						Stats: &statepb.Stats{
   585  							PassCount:  2,
   586  							FailCount:  1,
   587  							TotalCount: 3,
   588  						},
   589  					},
   590  					{
   591  						Name: "middle",
   592  						Stats: &statepb.Stats{
   593  							PassCount:  1,
   594  							FailCount:  2,
   595  							TotalCount: 3,
   596  							Broken:     true,
   597  						},
   598  					},
   599  					{
   600  						Name: "initial",
   601  						Stats: &statepb.Stats{
   602  							PassCount:  2,
   603  							FailCount:  1,
   604  							TotalCount: 3,
   605  						},
   606  					},
   607  				},
   608  				Rows: []*statepb.Row{
   609  					{
   610  						Name:    "okay",
   611  						Id:      "okay",
   612  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 3},
   613  					},
   614  					{
   615  						Name:    "broken",
   616  						Id:      "broken",
   617  						Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 3},
   618  						AlertInfo: &statepb.AlertInfo{
   619  							FailCount: 3,
   620  						},
   621  					},
   622  					{
   623  						Name:    "flaky",
   624  						Id:      "flaky",
   625  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1, int32(tspb.TestStatus_BUILD_FAIL), 1, int32(tspb.TestStatus_BUILD_PASSED), 1},
   626  					},
   627  				},
   628  			},
   629  		},
   630  		{
   631  			name: "calculate stats, no broken threshold",
   632  			grid: buildGrid(t,
   633  				updater.InflatedColumn{
   634  					Column: &statepb.Column{Name: "initial"},
   635  					Cells: map[string]updater.Cell{
   636  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   637  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   638  					},
   639  				}),
   640  			dashCfg: &configpb.DashboardTab{
   641  				Name: "tab",
   642  				AlertOptions: &configpb.DashboardTabAlertOptions{
   643  					NumFailuresToAlert:      1,
   644  					NumPassesToDisableAlert: 1,
   645  				},
   646  			},
   647  			groupCfg:       &configpb.TestGroup{},
   648  			useTabAlert:    true,
   649  			calculateStats: true,
   650  			expected: &statepb.Grid{
   651  				Columns: []*statepb.Column{
   652  					{
   653  						Name: "initial",
   654  					},
   655  				},
   656  				Rows: []*statepb.Row{
   657  					{
   658  						Name:    "okay",
   659  						Id:      "okay",
   660  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1},
   661  					},
   662  					{
   663  						Name:    "broken",
   664  						Id:      "broken",
   665  						Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 1},
   666  						AlertInfo: &statepb.AlertInfo{
   667  							FailCount: 1,
   668  						},
   669  					},
   670  				},
   671  			},
   672  		},
   673  		{
   674  			name: "calculate stats, calculate stats false",
   675  			grid: buildGrid(t,
   676  				updater.InflatedColumn{
   677  					Column: &statepb.Column{Name: "initial"},
   678  					Cells: map[string]updater.Cell{
   679  						"okay":   {Result: tspb.TestStatus_BUILD_PASSED},
   680  						"broken": {Result: tspb.TestStatus_BUILD_FAIL},
   681  					},
   682  				}),
   683  			dashCfg: &configpb.DashboardTab{
   684  				Name: "tab",
   685  				AlertOptions: &configpb.DashboardTabAlertOptions{
   686  					NumFailuresToAlert:      1,
   687  					NumPassesToDisableAlert: 1,
   688  				},
   689  				BrokenColumnThreshold: 0.5,
   690  			},
   691  			groupCfg:       &configpb.TestGroup{},
   692  			useTabAlert:    true,
   693  			calculateStats: false,
   694  			expected: &statepb.Grid{
   695  				Columns: []*statepb.Column{
   696  					{
   697  						Name: "initial",
   698  					},
   699  				},
   700  				Rows: []*statepb.Row{
   701  					{
   702  						Name:    "okay",
   703  						Id:      "okay",
   704  						Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1},
   705  					},
   706  					{
   707  						Name:    "broken",
   708  						Id:      "broken",
   709  						Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 1},
   710  						AlertInfo: &statepb.AlertInfo{
   711  							FailCount: 1,
   712  						},
   713  					},
   714  				},
   715  			},
   716  		},
   717  	}
   718  
   719  	for _, tc := range testcases {
   720  		t.Run(tc.name, func(t *testing.T) {
   721  			ctx, cancel := context.WithCancel(context.Background())
   722  			defer cancel()
   723  
   724  			actual, err := tabulate(ctx, logrus.New(), tc.grid, tc.dashCfg, tc.groupCfg, tc.calculateStats, tc.useTabAlert, nil) // TODO(slchase): add tests for not nil
   725  			if tc.expected == nil {
   726  				if err == nil {
   727  					t.Error("Expected an error, but got none")
   728  				}
   729  				return
   730  			}
   731  
   732  			if err != nil {
   733  				t.Fatalf("Unexpected error: %v", err)
   734  			}
   735  
   736  			diff := cmp.Diff(actual, tc.expected, protocmp.Transform(),
   737  				protocmp.IgnoreFields(&statepb.Row{}, "cell_ids", "icons", "messages", "user_property", "properties"), // mostly empty
   738  				protocmp.IgnoreFields(&statepb.AlertInfo{}, "fail_time"),                                              // import not needed to determine if alert was set
   739  				protocmp.SortRepeatedFields(&statepb.Grid{}, "rows"))                                                  // rows have no canonical order
   740  			if diff != "" {
   741  				t.Errorf("(-got, +want): %s", diff)
   742  			}
   743  		})
   744  	}
   745  }
   746  
   747  func buildGrid(t *testing.T, cols ...updater.InflatedColumn) *statepb.Grid {
   748  	t.Helper()
   749  	var g statepb.Grid
   750  	r := map[string]*statepb.Row{}
   751  	for _, col := range cols {
   752  		updater.AppendColumn(&g, r, col)
   753  	}
   754  	return &g
   755  }
   756  
   757  func Test_CreateTabState(t *testing.T) {
   758  	var exampleGrid statepb.Grid
   759  	updater.AppendColumn(&exampleGrid, map[string]*statepb.Row{}, updater.InflatedColumn{
   760  		Column: &statepb.Column{Name: "full"},
   761  		Cells: map[string]updater.Cell{
   762  			"some data": {Result: tspb.TestStatus_BUILD_PASSED},
   763  		},
   764  	})
   765  
   766  	testcases := []struct {
   767  		name         string
   768  		state        *statepb.Grid
   769  		confirm      bool
   770  		expectError  bool
   771  		expectUpload bool
   772  	}{
   773  		{
   774  			name:        "Fails if data is missing",
   775  			expectError: true,
   776  		},
   777  		{
   778  			name:        "Does not write without confirm",
   779  			state:       &exampleGrid,
   780  			confirm:     false,
   781  			expectError: false,
   782  		},
   783  		{
   784  			name:         "Writes data when upload is expected",
   785  			state:        &exampleGrid,
   786  			confirm:      true,
   787  			expectUpload: true,
   788  		},
   789  	}
   790  
   791  	expectedPath := newPathOrDie("gs://example/prefix/dashboard/tab")
   792  	configPath := newPathOrDie("gs://example/config")
   793  
   794  	for _, tc := range testcases {
   795  		t.Run(tc.name, func(t *testing.T) {
   796  			ctx, cancel := context.WithCancel(context.Background())
   797  			defer cancel()
   798  			client := fake.UploadClient{
   799  				Uploader: fake.Uploader{},
   800  			}
   801  
   802  			task := writeTask{
   803  				dashboard: &configpb.Dashboard{
   804  					Name: "dashboard",
   805  				},
   806  				tab: &configpb.DashboardTab{
   807  					Name: "tab",
   808  				},
   809  				group: &configpb.TestGroup{
   810  					Name: "testgroup",
   811  				},
   812  				data: tc.state,
   813  			}
   814  
   815  			err := createTabState(ctx, logrus.New(), client, task, *configPath, "prefix", tc.confirm, true, true, false)
   816  			if tc.expectError == (err == nil) {
   817  				t.Errorf("Wrong error: want %t, got %v", tc.expectError, err)
   818  			}
   819  			res, ok := client.Uploader[*expectedPath]
   820  			uploaded := ok && (len(res.Buf) != 0)
   821  			if uploaded != tc.expectUpload {
   822  				t.Errorf("Wrong upload: want %t, got %v", tc.expectUpload, ok)
   823  			}
   824  		})
   825  	}
   826  }
   827  
   828  func Test_MergeGrids(t *testing.T) {
   829  	testcases := []struct {
   830  		name    string
   831  		current []updater.InflatedColumn
   832  		add     []updater.InflatedColumn
   833  		expect  []updater.InflatedColumn
   834  	}{
   835  		{
   836  			name:    "Creating a grid",
   837  			current: []updater.InflatedColumn{},
   838  			add: []updater.InflatedColumn{
   839  				{
   840  					Column: &statepb.Column{
   841  						Name:    "cool results",
   842  						Started: 12345678,
   843  					},
   844  					Cells: map[string]updater.Cell{
   845  						"cell": {Result: tspb.TestStatus_PASS},
   846  					},
   847  				},
   848  				{
   849  					Column: &statepb.Column{
   850  						Name:    "result too big :(",
   851  						Started: 123456,
   852  					},
   853  					Cells: map[string]updater.Cell{
   854  						"cell": {Result: tspb.TestStatus_RUNNING},
   855  					},
   856  				},
   857  			},
   858  			expect: []updater.InflatedColumn{
   859  				{
   860  					Column: &statepb.Column{
   861  						Name:    "cool results",
   862  						Started: 12345678,
   863  					},
   864  					Cells: map[string]updater.Cell{
   865  						"cell": {Result: tspb.TestStatus_PASS},
   866  					},
   867  				},
   868  				{
   869  					Column: &statepb.Column{
   870  						Name:    "result too big :(",
   871  						Started: 123456,
   872  					},
   873  					Cells: map[string]updater.Cell{
   874  						"cell": {Result: tspb.TestStatus_RUNNING},
   875  					},
   876  				},
   877  			},
   878  		},
   879  		{
   880  			name: "two identical results: displays new result",
   881  			current: []updater.InflatedColumn{
   882  				{
   883  					Column: &statepb.Column{
   884  						Name:    "result 2",
   885  						Build:   "build",
   886  						Started: 1234,
   887  					},
   888  					Cells: map[string]updater.Cell{
   889  						"cell": {Result: tspb.TestStatus_RUNNING},
   890  					},
   891  				},
   892  				{
   893  					Column: &statepb.Column{
   894  						Name:    "result 1",
   895  						Build:   "build",
   896  						Started: 123,
   897  					},
   898  					Cells: map[string]updater.Cell{
   899  						"cell": {Result: tspb.TestStatus_RUNNING},
   900  					},
   901  				},
   902  			},
   903  			add: []updater.InflatedColumn{
   904  				{
   905  					Column: &statepb.Column{
   906  						Name:    "result 2",
   907  						Build:   "build",
   908  						Started: 1234,
   909  					},
   910  					Cells: map[string]updater.Cell{
   911  						"cell": {Result: tspb.TestStatus_PASS},
   912  					},
   913  				},
   914  				{
   915  					Column: &statepb.Column{
   916  						Name:    "result 1",
   917  						Build:   "build",
   918  						Started: 123,
   919  					},
   920  					Cells: map[string]updater.Cell{
   921  						"cell": {Result: tspb.TestStatus_PASS},
   922  					},
   923  				},
   924  				{
   925  					Column: &statepb.Column{
   926  						Name:    "too big",
   927  						Build:   "build",
   928  						Started: 12,
   929  					},
   930  					Cells: map[string]updater.Cell{
   931  						"cell": {Result: tspb.TestStatus_UNKNOWN},
   932  					},
   933  				},
   934  			},
   935  			expect: []updater.InflatedColumn{
   936  				{
   937  					Column: &statepb.Column{
   938  						Name:    "result 2",
   939  						Build:   "build",
   940  						Started: 1234,
   941  					},
   942  					Cells: map[string]updater.Cell{
   943  						"cell": {Result: tspb.TestStatus_PASS},
   944  					},
   945  				},
   946  				{
   947  					Column: &statepb.Column{
   948  						Name:    "result 1",
   949  						Build:   "build",
   950  						Started: 123,
   951  					},
   952  					Cells: map[string]updater.Cell{
   953  						"cell": {Result: tspb.TestStatus_PASS},
   954  					},
   955  				},
   956  				{
   957  					Column: &statepb.Column{
   958  						Name:    "too big",
   959  						Build:   "build",
   960  						Started: 12,
   961  					},
   962  					Cells: map[string]updater.Cell{
   963  						"cell": {Result: tspb.TestStatus_UNKNOWN},
   964  					},
   965  				},
   966  			},
   967  		},
   968  		{
   969  			name: "adds new info: keeps historical data",
   970  			current: []updater.InflatedColumn{
   971  				{
   972  					Column: &statepb.Column{
   973  						Name:    "fourth",
   974  						Started: 1234,
   975  					},
   976  					Cells: map[string]updater.Cell{
   977  						"cell": {Result: tspb.TestStatus_PASS},
   978  					},
   979  				},
   980  				{
   981  					Column: &statepb.Column{
   982  						Name:    "third",
   983  						Started: 123,
   984  					},
   985  					Cells: map[string]updater.Cell{
   986  						"cell": {Result: tspb.TestStatus_PASS},
   987  					},
   988  				},
   989  				{
   990  					Column: &statepb.Column{
   991  						Name:    "second",
   992  						Started: 12,
   993  					},
   994  					Cells: map[string]updater.Cell{
   995  						"cell": {Result: tspb.TestStatus_PASS},
   996  					},
   997  				},
   998  				{
   999  					Column: &statepb.Column{
  1000  						Name:    "first",
  1001  						Started: 1,
  1002  					},
  1003  					Cells: map[string]updater.Cell{
  1004  						"cell": {
  1005  							Result: tspb.TestStatus_UNKNOWN,
  1006  							Icon:   "...",
  1007  						},
  1008  					},
  1009  				},
  1010  			},
  1011  			add: []updater.InflatedColumn{
  1012  				{
  1013  					Column: &statepb.Column{
  1014  						Name:    "sixth",
  1015  						Started: 123456,
  1016  					},
  1017  					Cells: map[string]updater.Cell{
  1018  						"cell": {Result: tspb.TestStatus_PASS},
  1019  					},
  1020  				},
  1021  				{
  1022  					Column: &statepb.Column{
  1023  						Name:    "fifth",
  1024  						Started: 12345,
  1025  					},
  1026  					Cells: map[string]updater.Cell{
  1027  						"cell": {Result: tspb.TestStatus_PASS},
  1028  					},
  1029  				},
  1030  				{
  1031  					Column: &statepb.Column{
  1032  						Name:    "fourth",
  1033  						Started: 1234,
  1034  					},
  1035  					Cells: map[string]updater.Cell{
  1036  						"cell": {
  1037  							Result: tspb.TestStatus_UNKNOWN,
  1038  							Icon:   "...",
  1039  						},
  1040  					},
  1041  				},
  1042  			},
  1043  			expect: []updater.InflatedColumn{
  1044  				{
  1045  					Column: &statepb.Column{
  1046  						Name:    "sixth",
  1047  						Started: 123456,
  1048  					},
  1049  					Cells: map[string]updater.Cell{
  1050  						"cell": {Result: tspb.TestStatus_PASS},
  1051  					},
  1052  				},
  1053  				{
  1054  					Column: &statepb.Column{
  1055  						Name:    "fifth",
  1056  						Started: 12345,
  1057  					},
  1058  					Cells: map[string]updater.Cell{
  1059  						"cell": {Result: tspb.TestStatus_PASS},
  1060  					},
  1061  				},
  1062  				{
  1063  					Column: &statepb.Column{
  1064  						Name:    "fourth",
  1065  						Started: 1234,
  1066  					},
  1067  					Cells: map[string]updater.Cell{
  1068  						"cell": {Result: tspb.TestStatus_PASS},
  1069  					},
  1070  				},
  1071  				{
  1072  					Column: &statepb.Column{
  1073  						Name:    "third",
  1074  						Started: 123,
  1075  					},
  1076  					Cells: map[string]updater.Cell{
  1077  						"cell": {Result: tspb.TestStatus_PASS},
  1078  					},
  1079  				},
  1080  				{
  1081  					Column: &statepb.Column{
  1082  						Name:    "second",
  1083  						Started: 12,
  1084  					},
  1085  					Cells: map[string]updater.Cell{
  1086  						"cell": {Result: tspb.TestStatus_PASS},
  1087  					},
  1088  				},
  1089  				{
  1090  					Column: &statepb.Column{
  1091  						Name:    "first",
  1092  						Started: 1,
  1093  					},
  1094  					Cells: map[string]updater.Cell{
  1095  						"cell": {
  1096  							Result: tspb.TestStatus_UNKNOWN,
  1097  							Icon:   "...",
  1098  						},
  1099  					},
  1100  				},
  1101  			},
  1102  		},
  1103  	}
  1104  
  1105  	for _, test := range testcases {
  1106  		t.Run(test.name, func(t *testing.T) {
  1107  			actual := mergeGrids(test.current, test.add)
  1108  			if diff := cmp.Diff(actual, test.expect, protocmp.Transform()); diff != "" {
  1109  				t.Errorf("Unexpected (-got, +want): %s", diff)
  1110  				t.Logf("Got %#v", actual)
  1111  			}
  1112  		})
  1113  	}
  1114  }