go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/stability/query_stability_test.go (about)

     1  // Copyright 2024 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package stability
    16  
    17  import (
    18  	"fmt"
    19  	"testing"
    20  	"time"
    21  
    22  	"go.chromium.org/luci/server/span"
    23  
    24  	"go.chromium.org/luci/analysis/internal/testutil"
    25  	"go.chromium.org/luci/analysis/pbutil"
    26  	pb "go.chromium.org/luci/analysis/proto/v1"
    27  
    28  	. "github.com/smartystreets/goconvey/convey"
    29  	. "go.chromium.org/luci/common/testing/assertions"
    30  )
    31  
    32  func TestQueryStability(t *testing.T) {
    33  	Convey("QueryStability", t, func() {
    34  		ctx := testutil.IntegrationTestContext(t)
    35  
    36  		var1 := pbutil.Variant("key1", "val1", "key2", "val1")
    37  		var3 := pbutil.Variant("key1", "val2", "key2", "val2")
    38  
    39  		err := CreateQueryStabilityTestData(ctx)
    40  		So(err, ShouldBeNil)
    41  
    42  		opts := QueryStabilitySampleRequest()
    43  		expectedResult := QueryStabilitySampleResponse()
    44  		txn, cancel := span.ReadOnlyTransaction(ctx)
    45  		defer cancel()
    46  
    47  		Convey("Baseline", func() {
    48  			result, err := QueryStability(txn, opts)
    49  			So(err, ShouldBeNil)
    50  			So(result, ShouldResembleProto, expectedResult)
    51  		})
    52  		Convey("Flake analysis uses full 14 days if MinWindow unmet", func() {
    53  			opts.Criteria.FlakeRate.MinWindow = 100
    54  			result, err := QueryStability(txn, opts)
    55  			So(err, ShouldBeNil)
    56  			So(result, ShouldResembleProto, QueryStabilitySampleResponseLargeWindow())
    57  		})
    58  		Convey("Project filter works correctly", func() {
    59  			opts.Project = "none"
    60  			expectedResult = []*pb.TestVariantStabilityAnalysis{
    61  				emptyStabilityAnalysis("test_id", var1),
    62  				emptyStabilityAnalysis("test_id", var3),
    63  			}
    64  
    65  			result, err := QueryStability(txn, opts)
    66  			So(err, ShouldBeNil)
    67  			So(result, ShouldResembleProto, expectedResult)
    68  		})
    69  		Convey("Realm filter works correctly", func() {
    70  			// No data exists in this realm.
    71  			opts.SubRealms = []string{"otherrealm"}
    72  			expectedResult = []*pb.TestVariantStabilityAnalysis{
    73  				emptyStabilityAnalysis("test_id", var1),
    74  				emptyStabilityAnalysis("test_id", var3),
    75  			}
    76  
    77  			result, err := QueryStability(txn, opts)
    78  			So(err, ShouldBeNil)
    79  			So(result, ShouldResembleProto, expectedResult)
    80  		})
    81  		Convey("Works for tests without data", func() {
    82  			notExistsVariant := pbutil.Variant("key1", "val1", "key2", "not_exists")
    83  			opts.TestVariantPositions = append(opts.TestVariantPositions,
    84  				&pb.QueryTestVariantStabilityRequest_TestVariantPosition{
    85  					TestId:  "not_exists_test_id",
    86  					Variant: var1,
    87  					Sources: &pb.Sources{
    88  						GitilesCommit: &pb.GitilesCommit{
    89  							Host:       "mysources.googlesource.com",
    90  							Project:    "myproject/src",
    91  							Ref:        "refs/heads/mybranch",
    92  							CommitHash: "aabbccddeeff00112233aabbccddeeff00112233",
    93  							Position:   130,
    94  						},
    95  					},
    96  				},
    97  				&pb.QueryTestVariantStabilityRequest_TestVariantPosition{
    98  					TestId:  "test_id",
    99  					Variant: notExistsVariant,
   100  					Sources: &pb.Sources{
   101  						GitilesCommit: &pb.GitilesCommit{
   102  							Host:       "mysources.googlesource.com",
   103  							Project:    "myproject/src",
   104  							Ref:        "refs/heads/mybranch",
   105  							CommitHash: "aabbccddeeff00112233aabbccddeeff00112233",
   106  							Position:   130,
   107  						},
   108  					},
   109  				})
   110  
   111  			expectedResult = append(expectedResult,
   112  				emptyStabilityAnalysis("not_exists_test_id", var1),
   113  				emptyStabilityAnalysis("test_id", notExistsVariant))
   114  
   115  			result, err := QueryStability(txn, opts)
   116  			So(err, ShouldBeNil)
   117  			So(result, ShouldResembleProto, expectedResult)
   118  		})
   119  		Convey("Batching works correctly", func() {
   120  			// Ensure the order of test variants in the request and response
   121  			// remain correct even when there are multiple batches.
   122  			var expandedInput []*pb.QueryTestVariantStabilityRequest_TestVariantPosition
   123  			var expectedOutput []*pb.TestVariantStabilityAnalysis
   124  			for i := 0; i < batchSize; i++ {
   125  				testID := fmt.Sprintf("test_id_%v", i)
   126  				expandedInput = append(expandedInput, &pb.QueryTestVariantStabilityRequest_TestVariantPosition{
   127  					TestId:  testID,
   128  					Variant: var1,
   129  					Sources: &pb.Sources{
   130  						GitilesCommit: &pb.GitilesCommit{
   131  							Host:       "mysources.googlesource.com",
   132  							Project:    "myproject/src",
   133  							Ref:        "refs/heads/mybranch",
   134  							CommitHash: "aabbccddeeff00112233aabbccddeeff00112233",
   135  							Position:   130,
   136  						},
   137  					},
   138  				})
   139  				expectedOutput = append(expectedOutput, emptyStabilityAnalysis(testID, var1))
   140  			}
   141  
   142  			opts.TestVariantPositions = append(expandedInput, opts.TestVariantPositions...)
   143  			expectedResult = append(expectedOutput, expectedResult...)
   144  
   145  			result, err := QueryStability(txn, opts)
   146  			So(err, ShouldBeNil)
   147  			So(result, ShouldResembleProto, expectedResult)
   148  		})
   149  	})
   150  }
   151  
   152  // emptyStabilityAnalysis returns an empty stability analysis proto.
   153  func emptyStabilityAnalysis(testID string, variant *pb.Variant) *pb.TestVariantStabilityAnalysis {
   154  	return &pb.TestVariantStabilityAnalysis{
   155  		TestId:      testID,
   156  		Variant:     variant,
   157  		FailureRate: &pb.TestVariantStabilityAnalysis_FailureRate{},
   158  		FlakeRate:   &pb.TestVariantStabilityAnalysis_FlakeRate{},
   159  	}
   160  }
   161  
   162  func TestQueryStabilityHelpers(t *testing.T) {
   163  	Convey("flattenSourceVerdictsToRuns", t, func() {
   164  		unexpectedRun := run{expected: false}
   165  		expectedRun := run{expected: true}
   166  		Convey("With no verdicts", func() {
   167  			verdicts := []*sourceVerdict{}
   168  			result := flattenSourceVerdictsToRuns(verdicts)
   169  			So(result, ShouldHaveLength, 0)
   170  		})
   171  		Convey("With one unexpected run", func() {
   172  			verdicts := []*sourceVerdict{
   173  				{
   174  					UnexpectedRuns: 1,
   175  				},
   176  			}
   177  			result := flattenSourceVerdictsToRuns(verdicts)
   178  			So(result, ShouldResemble, []run{unexpectedRun})
   179  		})
   180  		Convey("With many unexpected runs", func() {
   181  			verdicts := []*sourceVerdict{
   182  				{
   183  					UnexpectedRuns: 3,
   184  				},
   185  			}
   186  			result := flattenSourceVerdictsToRuns(verdicts)
   187  			So(result, ShouldResemble, []run{unexpectedRun, unexpectedRun, unexpectedRun})
   188  		})
   189  		Convey("With one expected run", func() {
   190  			verdicts := []*sourceVerdict{
   191  				{
   192  					ExpectedRuns: 1,
   193  				},
   194  			}
   195  			result := flattenSourceVerdictsToRuns(verdicts)
   196  			So(result, ShouldResemble, []run{expectedRun})
   197  		})
   198  		Convey("With many expected run", func() {
   199  			verdicts := []*sourceVerdict{
   200  				{
   201  					ExpectedRuns: 3,
   202  				},
   203  			}
   204  			result := flattenSourceVerdictsToRuns(verdicts)
   205  			So(result, ShouldResemble, []run{expectedRun, expectedRun, expectedRun})
   206  		})
   207  		Convey("With mixed runs, evenly split", func() {
   208  			verdicts := []*sourceVerdict{
   209  				{
   210  					ExpectedRuns:   3,
   211  					UnexpectedRuns: 3,
   212  				},
   213  			}
   214  			result := flattenSourceVerdictsToRuns(verdicts)
   215  			So(result, ShouldResemble, []run{unexpectedRun, expectedRun, unexpectedRun, expectedRun, unexpectedRun, expectedRun})
   216  		})
   217  		Convey("With mixed runs, 3/2 split", func() {
   218  			verdicts := []*sourceVerdict{
   219  				{
   220  					ExpectedRuns:   4,
   221  					UnexpectedRuns: 2,
   222  				},
   223  			}
   224  			result := flattenSourceVerdictsToRuns(verdicts)
   225  			So(result, ShouldResemble, []run{unexpectedRun, expectedRun, expectedRun, unexpectedRun, expectedRun, expectedRun})
   226  		})
   227  		Convey("With multiple verdicts", func() {
   228  			verdicts := []*sourceVerdict{
   229  				{
   230  					ExpectedRuns: 2,
   231  				},
   232  				{
   233  					ExpectedRuns:   1,
   234  					UnexpectedRuns: 1,
   235  				},
   236  				{
   237  					UnexpectedRuns: 2,
   238  				},
   239  			}
   240  			result := flattenSourceVerdictsToRuns(verdicts)
   241  			So(result, ShouldResemble, []run{expectedRun, expectedRun, unexpectedRun, expectedRun, unexpectedRun, unexpectedRun})
   242  		})
   243  	})
   244  	Convey("truncateSourceVerdicts", t, func() {
   245  		Convey("With no verdicts", func() {
   246  			verdicts := []*sourceVerdict{}
   247  			result := truncateSourceVerdicts(verdicts, 10)
   248  			So(result, ShouldHaveLength, 0)
   249  		})
   250  		Convey("With large expected verdict", func() {
   251  			verdicts := []*sourceVerdict{
   252  				{
   253  					ExpectedRuns: 11,
   254  				},
   255  			}
   256  			result := truncateSourceVerdicts(verdicts, 10)
   257  			So(result, ShouldResemble, []*sourceVerdict{
   258  				{
   259  					ExpectedRuns: 10,
   260  				},
   261  			})
   262  		})
   263  		Convey("With large unexpected verdict", func() {
   264  			verdicts := []*sourceVerdict{
   265  				{
   266  					UnexpectedRuns: 111,
   267  				},
   268  			}
   269  			result := truncateSourceVerdicts(verdicts, 10)
   270  			So(result, ShouldResemble, []*sourceVerdict{
   271  				{
   272  					UnexpectedRuns: 10,
   273  				},
   274  			})
   275  		})
   276  		Convey("With multiple verdicts", func() {
   277  			verdicts := []*sourceVerdict{
   278  				{
   279  					ExpectedRuns: 2,
   280  				},
   281  				{
   282  					ExpectedRuns:   8,
   283  					UnexpectedRuns: 8,
   284  				},
   285  				{
   286  					UnexpectedRuns: 3,
   287  				},
   288  			}
   289  			result := truncateSourceVerdicts(verdicts, 10)
   290  			So(result, ShouldResemble, []*sourceVerdict{
   291  				{
   292  					ExpectedRuns: 2,
   293  				},
   294  				{
   295  					ExpectedRuns:   4,
   296  					UnexpectedRuns: 4,
   297  				},
   298  			})
   299  		})
   300  	})
   301  	Convey("consecutiveFailureCount", t, func() {
   302  		Convey("Consecutive from start and/or end", func() {
   303  			type testCase struct {
   304  				runs     []run
   305  				expected int
   306  			}
   307  
   308  			// Assume 10 runs, split 4 after / 2 on / 4 before.
   309  			testCases := []testCase{
   310  				{
   311  					runs:     expectedRuns(10),
   312  					expected: 0,
   313  				},
   314  				{
   315  					runs:     combine(unexpectedRuns(1), expectedRuns(9)),
   316  					expected: 0,
   317  				},
   318  				{
   319  					runs:     combine(unexpectedRuns(2), expectedRuns(8)),
   320  					expected: 0,
   321  				},
   322  				{
   323  					runs:     combine(unexpectedRuns(3), expectedRuns(7)),
   324  					expected: 0,
   325  				},
   326  				{
   327  					runs:     combine(unexpectedRuns(4), expectedRuns(6)),
   328  					expected: 0,
   329  				},
   330  				{
   331  					runs:     combine(unexpectedRuns(5), expectedRuns(5)),
   332  					expected: 0,
   333  				},
   334  				{
   335  					runs:     combine(unexpectedRuns(6), expectedRuns(4)),
   336  					expected: 6,
   337  				},
   338  				{
   339  					runs:     combine(unexpectedRuns(7), expectedRuns(3)),
   340  					expected: 7,
   341  				},
   342  				{
   343  					runs:     combine(unexpectedRuns(8), expectedRuns(2)),
   344  					expected: 8,
   345  				},
   346  				{
   347  					runs:     combine(unexpectedRuns(9), expectedRuns(1)),
   348  					expected: 9,
   349  				},
   350  				{
   351  					runs:     unexpectedRuns(10),
   352  					expected: 10,
   353  				},
   354  				{
   355  					runs:     combine(expectedRuns(1), unexpectedRuns(9)),
   356  					expected: 9,
   357  				},
   358  				{
   359  					runs:     combine(expectedRuns(2), unexpectedRuns(8)),
   360  					expected: 8,
   361  				},
   362  				{
   363  					runs:     combine(expectedRuns(3), unexpectedRuns(7)),
   364  					expected: 7,
   365  				},
   366  				{
   367  					runs:     combine(expectedRuns(4), unexpectedRuns(6)),
   368  					expected: 6,
   369  				},
   370  				{
   371  					runs:     combine(expectedRuns(5), unexpectedRuns(5)),
   372  					expected: 0,
   373  				},
   374  			}
   375  
   376  			for _, tc := range testCases {
   377  				afterRuns := tc.runs[:4]
   378  				onRuns := tc.runs[4:6]
   379  				beforeRuns := tc.runs[6:]
   380  				So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, tc.expected)
   381  			}
   382  		})
   383  		Convey("Consecutive runs do not touch start or end", func() {
   384  			runs := combine(expectedRuns(1), unexpectedRuns(8), expectedRuns(1))
   385  
   386  			afterRuns := runs[:4]
   387  			onRuns := runs[4:6]
   388  			beforeRuns := runs[6:]
   389  			So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, 0)
   390  		})
   391  		Convey("Consecutive unexpected runs on after side of queried position, no runs on queried position", func() {
   392  			runs := combine(unexpectedRuns(5), expectedRuns(5))
   393  
   394  			afterRuns := runs[:5]
   395  			onRuns := runs[5:5] // Empty slice
   396  			beforeRuns := runs[5:]
   397  			So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, 5)
   398  		})
   399  		Convey("Consecutive unexpected runs on before side of queried position, no runs on queried position", func() {
   400  			runs := combine(expectedRuns(5), unexpectedRuns(5))
   401  
   402  			afterRuns := runs[:5]
   403  			onRuns := runs[5:5] // Empty slice
   404  			beforeRuns := runs[5:]
   405  			So(consecutiveUnexpectedCount(afterRuns, onRuns, beforeRuns), ShouldEqual, 5)
   406  		})
   407  	})
   408  	Convey("unexpectedRunsInWindow", t, func() {
   409  		Convey("no runs", func() {
   410  			So(unexpectedRunsInWindow(nil, 10), ShouldEqual, 0)
   411  		})
   412  		Convey("fewer runs than window size", func() {
   413  			runs := combine(unexpectedRuns(3), expectedRuns(2), unexpectedRuns(2))
   414  			So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 5)
   415  		})
   416  		Convey("only expected runs", func() {
   417  			runs := expectedRuns(20)
   418  			So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 0)
   419  		})
   420  		Convey("only unexpected runs", func() {
   421  			runs := unexpectedRuns(20)
   422  			So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 10)
   423  		})
   424  		Convey("mixed runs", func() {
   425  			runs := combine(expectedRuns(5), unexpectedRuns(9), expectedRuns(6))
   426  			So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 9)
   427  		})
   428  		Convey("mixed runs 2", func() {
   429  			runs := combine(expectedRuns(1), unexpectedRuns(4), expectedRuns(3), unexpectedRuns(4), expectedRuns(9))
   430  			So(unexpectedRunsInWindow(runs, 10), ShouldEqual, 7)
   431  		})
   432  	})
   433  	Convey("queryBuckets", t, func() {
   434  		buckets := []*sourcePositionBucket{
   435  			{
   436  				StartSourcePosition:   2,
   437  				EndSourcePosition:     3,
   438  				EarliestPartitionTime: time.Date(2100, time.July, 1, 0, 0, 0, 0, time.UTC),
   439  			},
   440  			{
   441  				StartSourcePosition:   4,
   442  				EndSourcePosition:     6,
   443  				EarliestPartitionTime: time.Date(2100, time.July, 3, 0, 0, 0, 0, time.UTC),
   444  			},
   445  			{
   446  				StartSourcePosition:   8,
   447  				EndSourcePosition:     8,
   448  				EarliestPartitionTime: time.Date(2100, time.July, 6, 0, 0, 0, 0, time.UTC),
   449  			},
   450  			{
   451  				StartSourcePosition:   10,
   452  				EndSourcePosition:     13,
   453  				EarliestPartitionTime: time.Date(2100, time.July, 8, 0, 0, 0, 0, time.UTC),
   454  			},
   455  			{
   456  				StartSourcePosition:   18,
   457  				EndSourcePosition:     32,
   458  				EarliestPartitionTime: time.Date(2100, time.July, 14, 0, 0, 0, 0, time.UTC),
   459  				// Earliest availability: July 12th (due to bucket below).
   460  			},
   461  			{
   462  				StartSourcePosition:   34,
   463  				EndSourcePosition:     40,
   464  				EarliestPartitionTime: time.Date(2100, time.July, 12, 0, 0, 0, 0, time.UTC),
   465  			},
   466  		}
   467  		Convey("query at end", func() {
   468  			result := queryBuckets(buckets, 40, time.Hour*24*7)
   469  			So(result, ShouldResemble, buckets[2:])
   470  		})
   471  		Convey("query beyond end", func() {
   472  			result := queryBuckets(buckets, 50, time.Hour*24*7)
   473  			So(result, ShouldResemble, buckets[2:])
   474  		})
   475  		Convey("query in middle", func() {
   476  			result := queryBuckets(buckets, 8, time.Hour*24*7)
   477  			So(result, ShouldResemble, buckets)
   478  		})
   479  		Convey("query at start", func() {
   480  			result := queryBuckets(buckets, 2, time.Hour*24*7)
   481  			So(result, ShouldResemble, buckets[:4])
   482  		})
   483  		Convey("query before start", func() {
   484  			result := queryBuckets(buckets, 1, time.Hour*24*7)
   485  			So(result, ShouldResemble, buckets[:4])
   486  		})
   487  		Convey("query empty buckets", func() {
   488  			result := queryBuckets(nil, 50, time.Hour*24*7)
   489  			So(result, ShouldHaveLength, 0)
   490  		})
   491  	})
   492  }
   493  
   494  func unexpectedRuns(count int) []run {
   495  	var result []run
   496  	for i := 0; i < count; i++ {
   497  		result = append(result, run{expected: false})
   498  	}
   499  	return result
   500  }
   501  
   502  func expectedRuns(count int) []run {
   503  	var result []run
   504  	for i := 0; i < count; i++ {
   505  		result = append(result, run{expected: true})
   506  	}
   507  	return result
   508  }
   509  
   510  func combine(runs ...[]run) []run {
   511  	var result []run
   512  	for _, runs := range runs {
   513  		result = append(result, runs...)
   514  	}
   515  	return result
   516  }