go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/changepoints/bayesian/bayesian_test.go (about)

     1  // Copyright 2023 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 bayesian
    16  
    17  import (
    18  	"testing"
    19  
    20  	"go.chromium.org/luci/analysis/internal/changepoints/inputbuffer"
    21  
    22  	. "github.com/smartystreets/goconvey/convey"
    23  )
    24  
    25  func TestBayesianAnalysis(t *testing.T) {
    26  	a := ChangepointPredictor{
    27  		ChangepointLikelihood: 0.01,
    28  		HasUnexpectedPrior: BetaDistribution{
    29  			Alpha: 0.3,
    30  			Beta:  0.5,
    31  		},
    32  		UnexpectedAfterRetryPrior: BetaDistribution{
    33  			Alpha: 0.5,
    34  			Beta:  0.5,
    35  		},
    36  	}
    37  	Convey("Pass to fail transition 1", t, func() {
    38  		var (
    39  			positions     = []int{1, 2, 3, 4, 5, 6}
    40  			total         = []int{2, 2, 1, 1, 2, 2}
    41  			hasUnexpected = []int{0, 0, 0, 1, 2, 2}
    42  		)
    43  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
    44  		changePoints := a.identifyChangePoints(vs)
    45  		So(changePoints, ShouldResemble, []int{3})
    46  	})
    47  
    48  	Convey("Pass to fail transition 2", t, func() {
    49  		var (
    50  			positions     = []int{1, 2, 3, 4, 5, 6}
    51  			total         = []int{2, 2, 1, 1, 2, 2}
    52  			hasUnexpected = []int{0, 0, 1, 1, 2, 2}
    53  		)
    54  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
    55  		changePoints := a.identifyChangePoints(vs)
    56  		So(changePoints, ShouldResemble, []int{2})
    57  	})
    58  
    59  	Convey("Pass to flake transition", t, func() {
    60  		var (
    61  			positions     = []int{1, 1, 2, 2, 2, 2}
    62  			total         = []int{3, 3, 1, 2, 3, 3}
    63  			hasUnexpected = []int{0, 0, 0, 2, 3, 3}
    64  		)
    65  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
    66  		changePoints := a.identifyChangePoints(vs)
    67  		So(changePoints, ShouldResemble, []int{2})
    68  	})
    69  
    70  	Convey("Pass to fail to pass transition", t, func() {
    71  		var (
    72  			positions     = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
    73  			total         = []int{2, 2, 3, 2, 3, 1, 1, 2, 2, 3, 2, 3, 2, 2}
    74  			hasUnexpected = []int{0, 0, 0, 0, 0, 1, 1, 2, 2, 3, 2, 3, 0, 0}
    75  		)
    76  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
    77  		changePoints := a.identifyChangePoints(vs)
    78  		So(changePoints, ShouldResemble, []int{5, 12})
    79  	})
    80  
    81  	Convey("Pass to flake transition", t, func() {
    82  		var (
    83  			positions     = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
    84  			total         = []int{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}
    85  			hasUnexpected = []int{0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
    86  		)
    87  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
    88  		changePoints := a.identifyChangePoints(vs)
    89  		So(changePoints, ShouldResemble, []int{6})
    90  	})
    91  
    92  	Convey("Flake to fail transition", t, func() {
    93  		var (
    94  			positions     = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
    95  			total         = []int{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}
    96  			hasUnexpected = []int{1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}
    97  		)
    98  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
    99  		changePoints := a.identifyChangePoints(vs)
   100  		So(changePoints, ShouldResemble, []int{6})
   101  	})
   102  
   103  	Convey("Pass consistently", t, func() {
   104  		var (
   105  			positions     = []int{1, 2, 3, 4, 5, 6, 7, 8}
   106  			total         = []int{2, 2, 2, 2, 2, 2, 2, 2}
   107  			hasUnexpected = []int{0, 0, 0, 0, 0, 0, 0, 0}
   108  		)
   109  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
   110  		changePoints := a.identifyChangePoints(vs)
   111  		So(len(changePoints), ShouldEqual, 0)
   112  	})
   113  
   114  	Convey("Fail consistently", t, func() {
   115  		var (
   116  			positions     = []int{1, 2, 3, 4, 5, 6, 7, 8}
   117  			total         = []int{2, 2, 2, 2, 2, 2, 2, 2}
   118  			hasUnexpected = []int{2, 2, 2, 2, 2, 2, 2, 2}
   119  		)
   120  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
   121  		changePoints := a.identifyChangePoints(vs)
   122  		So(len(changePoints), ShouldEqual, 0)
   123  	})
   124  
   125  	Convey("Flake", t, func() {
   126  		var (
   127  			positions     = []int{1, 2, 3, 4, 5, 6, 7, 8}
   128  			total         = []int{2, 2, 2, 2, 2, 2, 2, 2}
   129  			hasUnexpected = []int{1, 0, 1, 0, 0, 1, 0, 2}
   130  		)
   131  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
   132  		changePoints := a.identifyChangePoints(vs)
   133  		So(len(changePoints), ShouldEqual, 0)
   134  	})
   135  
   136  	Convey("(Fail, Pass after retry) to (Fail, Fail after retry)", t, func() {
   137  		var (
   138  			positions            = []int{1, 2, 3, 4, 5, 6, 7, 8}
   139  			total                = []int{2, 2, 2, 2, 2, 2, 2, 2}
   140  			hasUnexpected        = []int{2, 2, 2, 2, 2, 2, 2, 2}
   141  			retries              = []int{2, 2, 2, 2, 2, 2, 2, 2}
   142  			unexpectedAfterRetry = []int{0, 0, 0, 0, 2, 2, 2, 2}
   143  		)
   144  		vs := inputbuffer.VerdictsWithRetries(positions, total, hasUnexpected, retries, unexpectedAfterRetry)
   145  		changePoints := a.identifyChangePoints(vs)
   146  		So(changePoints, ShouldResemble, []int{4})
   147  	})
   148  
   149  	Convey("(Fail, Fail after retry) consistently", t, func() {
   150  		var (
   151  			positions            = []int{1, 2, 3, 4, 5, 6, 7, 8}
   152  			total                = []int{2, 2, 2, 2, 2, 2, 2, 2}
   153  			hasUnexpected        = []int{2, 2, 2, 2, 2, 2, 2, 2}
   154  			retries              = []int{2, 2, 2, 2, 2, 2, 2, 2}
   155  			unexpectedAfterRetry = []int{2, 2, 2, 2, 2, 2, 2, 2}
   156  		)
   157  		vs := inputbuffer.VerdictsWithRetries(positions, total, hasUnexpected, retries, unexpectedAfterRetry)
   158  		changePoints := a.identifyChangePoints(vs)
   159  		So(len(changePoints), ShouldEqual, 0)
   160  	})
   161  
   162  	Convey("(Fail, Fail after retry) to (Fail, Flaky on retry)", t, func() {
   163  		var (
   164  			// The change point should be detected between commit positions 3 and 5.
   165  			positions            = []int{1, 2, 3, 5, 5, 5, 7, 7}
   166  			total                = []int{3, 3, 3, 1, 3, 3, 3, 3}
   167  			hasUnexpected        = []int{3, 3, 3, 1, 3, 3, 3, 3}
   168  			retries              = []int{3, 3, 3, 1, 3, 3, 3, 3}
   169  			unexpectedAfterRetry = []int{3, 3, 3, 1, 0, 0, 1, 1}
   170  		)
   171  		vs := inputbuffer.VerdictsWithRetries(positions, total, hasUnexpected, retries, unexpectedAfterRetry)
   172  		changePoints := a.identifyChangePoints(vs)
   173  		So(changePoints, ShouldResemble, []int{3})
   174  	})
   175  }
   176  
   177  // Output as of March 2023 on Intel Skylake CPU @ 2.00GHz:
   178  // BenchmarkBayesianAnalysisConsistentPass-48    	   30054	     39879 ns/op	      18 B/op	       0 allocs/op
   179  func BenchmarkBayesianAnalysisConsistentPass(b *testing.B) {
   180  	a := ChangepointPredictor{
   181  		ChangepointLikelihood: 0.01,
   182  		HasUnexpectedPrior: BetaDistribution{
   183  			Alpha: 0.3,
   184  			Beta:  0.5,
   185  		},
   186  		UnexpectedAfterRetryPrior: BetaDistribution{
   187  			Alpha: 0.5,
   188  			Beta:  0.5,
   189  		},
   190  	}
   191  
   192  	var vs []inputbuffer.PositionVerdict
   193  
   194  	// Consistently passing test. This represents ~99% of tests.
   195  	for i := 0; i < 2000; i++ {
   196  		vs = append(vs, inputbuffer.PositionVerdict{
   197  			CommitPosition:       i,
   198  			IsSimpleExpectedPass: true,
   199  		})
   200  	}
   201  	for i := 0; i < b.N; i++ {
   202  		result := a.identifyChangePoints(vs)
   203  		if len(result) != 0 {
   204  			panic("unexpected result")
   205  		}
   206  	}
   207  }
   208  
   209  // Output as of March 2023 on Intel Skylake CPU @ 2.00GHz:
   210  // BenchmarkBayesianAnalysisFlaky-48    	    1500	    796446 ns/op	     396 B/op	       0 allocs/op
   211  func BenchmarkBayesianAnalysisFlaky(b *testing.B) {
   212  	a := ChangepointPredictor{
   213  		ChangepointLikelihood: 0.01,
   214  		HasUnexpectedPrior: BetaDistribution{
   215  			Alpha: 0.3,
   216  			Beta:  0.5,
   217  		},
   218  		UnexpectedAfterRetryPrior: BetaDistribution{
   219  			Alpha: 0.5,
   220  			Beta:  0.5,
   221  		},
   222  	}
   223  	// Flaky test.
   224  	var vs []inputbuffer.PositionVerdict
   225  	for i := 0; i < 2000; i++ {
   226  		if i%2 == 0 {
   227  			vs = append(vs, inputbuffer.PositionVerdict{
   228  				CommitPosition:       i,
   229  				IsSimpleExpectedPass: true,
   230  			})
   231  		} else {
   232  			vs = append(vs, inputbuffer.PositionVerdict{
   233  				CommitPosition: i,
   234  				Details: inputbuffer.VerdictDetails{
   235  					Runs: []inputbuffer.Run{
   236  						{
   237  							Expected: inputbuffer.ResultCounts{
   238  								PassCount: 1,
   239  							},
   240  							Unexpected: inputbuffer.ResultCounts{
   241  								FailCount: 1,
   242  							},
   243  						},
   244  					},
   245  				},
   246  			})
   247  		}
   248  	}
   249  	for i := 0; i < b.N; i++ {
   250  		result := a.identifyChangePoints(vs)
   251  		if len(result) != 0 {
   252  			panic("unexpected result")
   253  		}
   254  	}
   255  }