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

     1  /*
     2  Copyright 2020 The TestGrid Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package summarizer
    18  
    19  import (
    20  	"testing"
    21  
    22  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    23  	summarypb "github.com/GoogleCloudPlatform/testgrid/pb/summary"
    24  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    25  	"github.com/GoogleCloudPlatform/testgrid/pkg/summarizer/analyzers"
    26  	"github.com/GoogleCloudPlatform/testgrid/pkg/summarizer/common"
    27  	"github.com/golang/protobuf/proto"
    28  	"github.com/google/go-cmp/cmp"
    29  	"github.com/google/go-cmp/cmp/cmpopts"
    30  )
    31  
    32  func TestCalculateTrend(t *testing.T) {
    33  	cases := []struct {
    34  		name                string
    35  		currentHealthiness  *summarypb.HealthinessInfo
    36  		previousHealthiness *summarypb.HealthinessInfo
    37  		expected            *summarypb.HealthinessInfo
    38  	}{
    39  		{
    40  			name: "typical input assigns correct ChangeFromLastInterval's",
    41  			currentHealthiness: &summarypb.HealthinessInfo{
    42  				Tests: []*summarypb.TestInfo{
    43  					{
    44  						DisplayName: "test2_should_be_DOWN",
    45  						Flakiness:   30.0,
    46  					},
    47  					{
    48  						DisplayName: "test1_should_be_UP",
    49  						Flakiness:   70.0,
    50  					},
    51  					{
    52  						DisplayName: "test3_should_be_NO_CHANGE",
    53  						Flakiness:   50.0,
    54  					},
    55  				},
    56  			},
    57  			previousHealthiness: &summarypb.HealthinessInfo{
    58  				Tests: []*summarypb.TestInfo{
    59  					{
    60  						DisplayName: "test1_should_be_UP",
    61  						Flakiness:   50.0,
    62  					},
    63  					{
    64  						DisplayName: "test2_should_be_DOWN",
    65  						Flakiness:   50.0,
    66  					},
    67  					{
    68  						DisplayName: "test3_should_be_NO_CHANGE",
    69  						Flakiness:   50.0,
    70  					},
    71  				},
    72  			},
    73  			expected: &summarypb.HealthinessInfo{
    74  				Tests: []*summarypb.TestInfo{
    75  					{
    76  						DisplayName:            "test2_should_be_DOWN",
    77  						Flakiness:              30.0,
    78  						PreviousFlakiness:      []float32{50.0},
    79  						ChangeFromLastInterval: summarypb.TestInfo_DOWN,
    80  					},
    81  					{
    82  						DisplayName:            "test1_should_be_UP",
    83  						Flakiness:              70.0,
    84  						PreviousFlakiness:      []float32{50.0},
    85  						ChangeFromLastInterval: summarypb.TestInfo_UP,
    86  					},
    87  					{
    88  						DisplayName:            "test3_should_be_NO_CHANGE",
    89  						Flakiness:              50.0,
    90  						PreviousFlakiness:      []float32{50.0},
    91  						ChangeFromLastInterval: summarypb.TestInfo_NO_CHANGE,
    92  					},
    93  				},
    94  			},
    95  		},
    96  	}
    97  
    98  	for _, tc := range cases {
    99  		t.Run(tc.name, func(t *testing.T) {
   100  			if CalculateTrend(tc.currentHealthiness, tc.previousHealthiness); !proto.Equal(tc.currentHealthiness, tc.expected) {
   101  				for _, expectedTest := range tc.expected.Tests {
   102  					// Linear search because the test cases are so small
   103  					for _, actualTest := range tc.currentHealthiness.Tests {
   104  						if actualTest.DisplayName != expectedTest.DisplayName {
   105  							continue
   106  						}
   107  						actual := actualTest.ChangeFromLastInterval
   108  						expected := expectedTest.ChangeFromLastInterval
   109  						if actual == expected {
   110  							continue
   111  						}
   112  
   113  						actualValue := int(actualTest.ChangeFromLastInterval)
   114  						expectedValue := int(expectedTest.ChangeFromLastInterval)
   115  						t.Logf("test: %s has trend of: %s (value: %d) but expected %s (value: %d)",
   116  							actualTest.DisplayName, actual, actualValue, expected, expectedValue)
   117  					}
   118  				}
   119  				t.Fail()
   120  			}
   121  		})
   122  	}
   123  }
   124  
   125  func TestGetTrend(t *testing.T) {
   126  	cases := []struct {
   127  		name              string
   128  		currentFlakiness  float32
   129  		previousFlakiness float32
   130  		expected          summarypb.TestInfo_Trend
   131  	}{
   132  		{
   133  			name:              "lower currentFlakiness returns TestInfo_DOWN",
   134  			currentFlakiness:  10.0,
   135  			previousFlakiness: 20.0,
   136  			expected:          summarypb.TestInfo_DOWN,
   137  		},
   138  		{
   139  			name:              "higher currentFlakiness returns TestInfo_UP",
   140  			currentFlakiness:  20.0,
   141  			previousFlakiness: 10.0,
   142  			expected:          summarypb.TestInfo_UP,
   143  		},
   144  		{
   145  			name:              "equal currentFlakiness and previousFlakiness returns TestInfo_NO_CHANGE",
   146  			currentFlakiness:  5.0,
   147  			previousFlakiness: 5.0,
   148  			expected:          summarypb.TestInfo_NO_CHANGE,
   149  		},
   150  	}
   151  	for _, tc := range cases {
   152  		t.Run(tc.name, func(t *testing.T) {
   153  			if actual := getTrend(tc.currentFlakiness, tc.previousFlakiness); actual != tc.expected {
   154  				t.Errorf("getTrend returned actual: %d != expected: %d for inputs (%f, %f)", actual, tc.expected, tc.currentFlakiness, tc.previousFlakiness)
   155  			}
   156  		})
   157  	}
   158  }
   159  
   160  func TestIsWithinTimeFrame(t *testing.T) {
   161  	cases := []struct {
   162  		name      string
   163  		column    *statepb.Column
   164  		startTime int
   165  		endTime   int
   166  		expected  bool
   167  	}{
   168  		{
   169  			name: "column within time frame returns true",
   170  			column: &statepb.Column{
   171  				Started: 1.0,
   172  			},
   173  			startTime: 0,
   174  			endTime:   2,
   175  			expected:  true,
   176  		},
   177  		{
   178  			name: "column before time frame returns false",
   179  			column: &statepb.Column{
   180  				Started: 1.0,
   181  			},
   182  			startTime: 3,
   183  			endTime:   7,
   184  			expected:  false,
   185  		},
   186  		{
   187  			name: "column after time frame returns false",
   188  			column: &statepb.Column{
   189  				Started: 4.0,
   190  			},
   191  			startTime: 0,
   192  			endTime:   2,
   193  			expected:  false,
   194  		},
   195  		{
   196  			name: "function is inclusive with column at start time",
   197  			column: &statepb.Column{
   198  				Started: 0.0,
   199  			},
   200  			startTime: 0,
   201  			endTime:   2,
   202  			expected:  true,
   203  		},
   204  		{
   205  			name: "function is inclusive with column at end time",
   206  			column: &statepb.Column{
   207  				Started: 2.0,
   208  			},
   209  			startTime: 0,
   210  			endTime:   2,
   211  			expected:  true,
   212  		},
   213  	}
   214  
   215  	for _, tc := range cases {
   216  		t.Run(tc.name, func(t *testing.T) {
   217  			if actual := isWithinTimeFrame(tc.column, tc.startTime, tc.endTime); actual != tc.expected {
   218  				t.Errorf("isWithinTimeFrame returned %t for %d < %f <= %d", actual, tc.startTime, tc.column.Started, tc.endTime)
   219  			}
   220  		})
   221  	}
   222  }
   223  
   224  func TestParseGrid(t *testing.T) {
   225  	cases := []struct {
   226  		name                   string
   227  		grid                   *statepb.Grid
   228  		startTime              int
   229  		endTime                int
   230  		expectedMetrics        []*common.GridMetrics
   231  		expectedFilteredStatus map[string][]analyzers.StatusCategory
   232  	}{
   233  		{
   234  			name: "grid with all analyzed result types produces correct result list",
   235  			grid: &statepb.Grid{
   236  				Columns: []*statepb.Column{
   237  					{Started: 0},
   238  					{Started: 1000},
   239  					{Started: 2000},
   240  					{Started: 2000},
   241  				},
   242  				Rows: []*statepb.Row{
   243  					{
   244  						Name: "test_1",
   245  						Results: []int32{
   246  							statuspb.TestStatus_value["PASS"], 1,
   247  							statuspb.TestStatus_value["FAIL"], 1,
   248  							statuspb.TestStatus_value["FLAKY"], 1,
   249  							statuspb.TestStatus_value["CATEGORIZED_FAIL"], 1,
   250  						},
   251  						Messages: []string{
   252  							"",
   253  							"",
   254  							"",
   255  							"infra_fail_1",
   256  						},
   257  					},
   258  				},
   259  			},
   260  			startTime: 0,
   261  			endTime:   2,
   262  			expectedMetrics: []*common.GridMetrics{
   263  				{
   264  					Name:             "test_1",
   265  					Passed:           1,
   266  					Failed:           1,
   267  					FlakyCount:       1,
   268  					AverageFlakiness: 50.0,
   269  					FailedInfraCount: 1,
   270  					InfraFailures: map[string]int{
   271  						"infra_fail_1": 1,
   272  					},
   273  				},
   274  			},
   275  			expectedFilteredStatus: map[string][]analyzers.StatusCategory{
   276  				"test_1": {
   277  					analyzers.StatusPass, analyzers.StatusFail, analyzers.StatusFlaky,
   278  				},
   279  			},
   280  		},
   281  		{
   282  			name: "grid with failing columns produces correct status list",
   283  			grid: &statepb.Grid{
   284  				Columns: []*statepb.Column{
   285  					{Started: 0},
   286  					{Started: 1000},
   287  					{Started: 2000},
   288  					{Started: 2000},
   289  				},
   290  				Rows: []*statepb.Row{
   291  					{
   292  						Name: "test_1",
   293  						Results: []int32{
   294  							statuspb.TestStatus_value["PASS"], 1,
   295  							statuspb.TestStatus_value["FAIL"], 1,
   296  							statuspb.TestStatus_value["FLAKY"], 1,
   297  							statuspb.TestStatus_value["CATEGORIZED_FAIL"], 1,
   298  						},
   299  						Messages: []string{
   300  							"",
   301  							"",
   302  							"",
   303  							"infra_fail_1",
   304  						},
   305  					},
   306  					{
   307  						Name: "test_2",
   308  						Results: []int32{
   309  							statuspb.TestStatus_value["PASS"], 1,
   310  							statuspb.TestStatus_value["FAIL"], 1,
   311  							statuspb.TestStatus_value["FAIL"], 1,
   312  							statuspb.TestStatus_value["CATEGORIZED_FAIL"], 1,
   313  						},
   314  						Messages: []string{
   315  							"",
   316  							"",
   317  							"",
   318  							"infra_fail_1",
   319  						},
   320  					},
   321  				},
   322  			},
   323  			startTime: 0,
   324  			endTime:   2,
   325  			expectedMetrics: []*common.GridMetrics{
   326  				{
   327  					Name:             "test_1",
   328  					Passed:           1,
   329  					Failed:           1,
   330  					FlakyCount:       1,
   331  					AverageFlakiness: 50.0,
   332  					FailedInfraCount: 1,
   333  					InfraFailures: map[string]int{
   334  						"infra_fail_1": 1,
   335  					},
   336  				},
   337  				{
   338  					Name:             "test_2",
   339  					Passed:           1,
   340  					Failed:           2,
   341  					FlakyCount:       0,
   342  					AverageFlakiness: 2 / 3,
   343  					FailedInfraCount: 1,
   344  					InfraFailures: map[string]int{
   345  						"infra_fail_1": 1,
   346  					},
   347  				},
   348  			},
   349  			expectedFilteredStatus: map[string][]analyzers.StatusCategory{
   350  				"test_1": {
   351  					analyzers.StatusPass, analyzers.StatusFlaky,
   352  				},
   353  				"test_2": {
   354  					analyzers.StatusPass, analyzers.StatusFail,
   355  				},
   356  			},
   357  		},
   358  		{
   359  			name: "grid with no analyzed results produces empty result list",
   360  			grid: &statepb.Grid{
   361  				Columns: []*statepb.Column{
   362  					{Started: -1000},
   363  					{Started: 1000},
   364  					{Started: 2000},
   365  					{Started: 2000},
   366  				},
   367  				Rows: []*statepb.Row{
   368  					{
   369  						Name: "test_1",
   370  						Results: []int32{
   371  							statuspb.TestStatus_value["NO_RESULT"], 4,
   372  						},
   373  						Messages: []string{
   374  							"this_message_should_not_show_up_in_results_0",
   375  							"this_message_should_not_show_up_in_results_1",
   376  							"this_message_should_not_show_up_in_results_2",
   377  							"this_message_should_not_show_up_in_results_3",
   378  						},
   379  					},
   380  				},
   381  			},
   382  			startTime:       0,
   383  			endTime:         2,
   384  			expectedMetrics: []*common.GridMetrics{},
   385  			expectedFilteredStatus: map[string][]analyzers.StatusCategory{
   386  				"test_1": {},
   387  			},
   388  		},
   389  		{
   390  			name: "grid with some non-analyzed results properly assigns correct messages",
   391  			grid: &statepb.Grid{
   392  				Columns: []*statepb.Column{
   393  					{Started: 0},
   394  					{Started: 1000},
   395  					{Started: 1000},
   396  					{Started: 2000},
   397  					{Started: 2000},
   398  				},
   399  				Rows: []*statepb.Row{
   400  					{
   401  						Name: "test_1",
   402  						Results: []int32{
   403  							statuspb.TestStatus_value["PASS"], 1,
   404  							statuspb.TestStatus_value["NO_RESULT"], 2,
   405  							statuspb.TestStatus_value["FAIL"], 2,
   406  						},
   407  						Messages: []string{
   408  							"this_message_should_not_show_up_in_results",
   409  							"this_message_should_show_up_as_an_infra_failure",
   410  							"",
   411  						},
   412  					},
   413  				},
   414  			},
   415  			startTime: 0,
   416  			endTime:   2,
   417  			expectedMetrics: []*common.GridMetrics{
   418  				{
   419  					Name:             "test_1",
   420  					Passed:           1,
   421  					Failed:           1,
   422  					FlakyCount:       0,
   423  					AverageFlakiness: 0.0,
   424  					FailedInfraCount: 1,
   425  					InfraFailures: map[string]int{
   426  						"this_message_should_show_up_as_an_infra_failure": 1,
   427  					},
   428  				},
   429  			},
   430  			expectedFilteredStatus: map[string][]analyzers.StatusCategory{
   431  				"test_1": {
   432  					analyzers.StatusPass, analyzers.StatusFail,
   433  				},
   434  			},
   435  		},
   436  		{
   437  			name: "grid with columns outside of time frame correctly assigns messages",
   438  			grid: &statepb.Grid{
   439  				Columns: []*statepb.Column{
   440  					{Started: 0},
   441  					{Started: 1000},
   442  					{Started: 1000},
   443  					{Started: 7000},
   444  					{Started: 2000},
   445  				},
   446  				Rows: []*statepb.Row{
   447  					{
   448  						Name: "test_1",
   449  						Results: []int32{
   450  							statuspb.TestStatus_value["PASS"], 1,
   451  							statuspb.TestStatus_value["NO_RESULT"], 2,
   452  							statuspb.TestStatus_value["FAIL"], 2,
   453  						},
   454  						Messages: []string{
   455  							"this_message_should_not_show_up_in_results",
   456  							"this_message_should_not_show_up_in_results",
   457  							"this_message_should_show_up_as_an_infra_failure",
   458  						},
   459  					},
   460  				},
   461  			},
   462  			startTime: 0,
   463  			endTime:   2,
   464  			expectedMetrics: []*common.GridMetrics{
   465  				{
   466  					Name:             "test_1",
   467  					Passed:           1,
   468  					Failed:           0,
   469  					FlakyCount:       0,
   470  					AverageFlakiness: 0.0,
   471  					FailedInfraCount: 1,
   472  					InfraFailures: map[string]int{
   473  						"this_message_should_show_up_as_an_infra_failure": 1,
   474  					},
   475  				},
   476  			},
   477  			expectedFilteredStatus: map[string][]analyzers.StatusCategory{
   478  				"test_1": {
   479  					analyzers.StatusPass,
   480  				},
   481  			},
   482  		},
   483  	}
   484  
   485  	metricsSort := func(x *common.GridMetrics, y *common.GridMetrics) bool {
   486  		return x.Name < y.Name
   487  	}
   488  
   489  	for _, tc := range cases {
   490  		t.Run(tc.name, func(t *testing.T) {
   491  			actualMetrics, actualFS := parseGrid(tc.grid, tc.startTime, tc.endTime)
   492  			if diff := cmp.Diff(tc.expectedMetrics, actualMetrics, cmpopts.SortSlices(metricsSort)); diff != "" {
   493  				t.Errorf("Metrics disagree (-want +got):\n%s", diff)
   494  			}
   495  			if diff := cmp.Diff(tc.expectedFilteredStatus, actualFS); diff != "" {
   496  				t.Errorf("Status disagree (-want +got):\n%s", diff)
   497  			}
   498  		})
   499  	}
   500  }
   501  
   502  func TestIsInfraFailure(t *testing.T) {
   503  	cases := []struct {
   504  		name     string
   505  		message  string
   506  		expected bool
   507  	}{
   508  		{
   509  			name:     "typical matched string increments counts correctly",
   510  			message:  "whatever_valid_word_character_string_with_no_spaces",
   511  			expected: true,
   512  		},
   513  		{
   514  			name:     "unmatched string increments Failed and not other counts",
   515  			message:  "message with spaces should no get matched",
   516  			expected: false,
   517  		},
   518  	}
   519  
   520  	for _, tc := range cases {
   521  		t.Run(tc.name, func(t *testing.T) {
   522  			if actual := isInfraFailure(tc.message); actual != tc.expected {
   523  				t.Errorf("isInfraFailure(%v) gave %t but want %t", tc.message, actual, tc.expected)
   524  			}
   525  		})
   526  	}
   527  }
   528  
   529  func TestIsValidTestName(t *testing.T) {
   530  	cases := []struct {
   531  		name     string
   532  		testName string
   533  		expected bool
   534  	}{
   535  		{
   536  			name:     "regular name returns true",
   537  			testName: "valid_test",
   538  			expected: true,
   539  		},
   540  		{
   541  			name:     "name with substring '@TESTGRID@' returns false",
   542  			testName: "invalid_test_@TESTGRID@",
   543  			expected: false,
   544  		},
   545  	}
   546  	for _, tc := range cases {
   547  		t.Run(tc.name, func(t *testing.T) {
   548  			if actual := isValidTestName(tc.testName); actual != tc.expected {
   549  				t.Errorf("isValidTestName returned %t for the name %s, but expected %t", actual, tc.testName, tc.expected)
   550  			}
   551  		})
   552  	}
   553  }
   554  
   555  func TestFailingColumns(t *testing.T) {
   556  	p := statuspb.TestStatus_value["PASS"]
   557  	f := statuspb.TestStatus_value["FAIL"]
   558  	fl := statuspb.TestStatus_value["FLAKY"]
   559  	cases := []struct {
   560  		name       string
   561  		rows       []*statepb.Row
   562  		numColumns int
   563  		expected   []bool
   564  	}{
   565  		{
   566  			name: "Some failing columns",
   567  			rows: []*statepb.Row{
   568  				{
   569  					Name: "//test1 - [env1]",
   570  					Results: []int32{
   571  						p, 1, f, 1, p, 1, p, 1, f, 1,
   572  					},
   573  				},
   574  				{
   575  					Name: "//test2 - [env1]",
   576  					Results: []int32{
   577  						p, 1, f, 1, p, 1, p, 1, f, 1,
   578  					},
   579  				},
   580  				{
   581  					Name: "//test3 - [env1]",
   582  					Results: []int32{
   583  						p, 1, f, 1, p, 1, p, 1, fl, 1,
   584  					},
   585  				},
   586  				{
   587  					Name: "//test4 - [env1]",
   588  					Results: []int32{
   589  						p, 1, f, 1, p, 1, p, 1, f, 1,
   590  					},
   591  				},
   592  			},
   593  			numColumns: 5,
   594  			expected:   []bool{false, true, false, false, false},
   595  		},
   596  		{
   597  			name: "Unequal Length rows",
   598  			rows: []*statepb.Row{
   599  				{
   600  					Name: "//test1 - [env1]",
   601  					Results: []int32{
   602  						p, 1, f, 1, p, 1,
   603  					},
   604  				},
   605  				{
   606  					Name: "//test2 - [env1]",
   607  					Results: []int32{
   608  						p, 1, f, 1,
   609  					},
   610  				},
   611  				{
   612  					Name: "//test3 - [env1]",
   613  					Results: []int32{
   614  						p, 1, f, 1, p, 1, p, 1,
   615  					},
   616  				},
   617  			},
   618  			numColumns: 3,
   619  			expected:   []bool{false, true, false},
   620  		},
   621  		{
   622  			name: "Only one test",
   623  			rows: []*statepb.Row{
   624  				{
   625  					Name: "//test1 - [env1]",
   626  					Results: []int32{
   627  						p, 1, f, 1, p, 1,
   628  					},
   629  				},
   630  			},
   631  			numColumns: 3,
   632  			expected:   []bool{false, false, false},
   633  		},
   634  	}
   635  	for _, tc := range cases {
   636  		t.Run(tc.name, func(t *testing.T) {
   637  			actual := failingColumns(tc.numColumns, tc.rows)
   638  			if diff := cmp.Diff(tc.expected, actual); diff != "" {
   639  				t.Errorf("failingColumns(ctx, %v %v) gave unexpected diff (-want +got): %s", tc.numColumns, tc.rows, diff)
   640  			}
   641  		})
   642  	}
   643  }