go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/changepoints_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 rpc
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"cloud.google.com/go/bigquery"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/server/auth"
    28  	"go.chromium.org/luci/server/auth/authtest"
    29  
    30  	"go.chromium.org/luci/analysis/internal/changepoints"
    31  	pb "go.chromium.org/luci/analysis/proto/v1"
    32  
    33  	. "github.com/smartystreets/goconvey/convey"
    34  	. "go.chromium.org/luci/common/testing/assertions"
    35  )
    36  
    37  func TestChangepointsServer(t *testing.T) {
    38  	Convey("TestChangepointsServer", t, func() {
    39  		ctx := context.Background()
    40  		ctx = auth.WithState(ctx, &authtest.FakeState{
    41  			Identity:       "user:someone@example.com",
    42  			IdentityGroups: []string{"luci-analysis-access"},
    43  		})
    44  		client := fakeChangepointClient{}
    45  		server := NewChangepointsServer(&client)
    46  		Convey("QueryChangepointGroupSummaries", func() {
    47  			Convey("unauthorised requests are rejected", func() {
    48  				req := &pb.QueryChangepointGroupSummariesRequest{
    49  					Project: "chromium",
    50  				}
    51  
    52  				res, err := server.QueryChangepointGroupSummaries(ctx, req)
    53  				So(err, ShouldErrLike, `not a member of googlers`)
    54  				So(err, ShouldHaveGRPCStatus, codes.PermissionDenied)
    55  				So(res, ShouldBeNil)
    56  			})
    57  			Convey("invalid requests are rejected", func() {
    58  				ctx = auth.WithState(ctx, &authtest.FakeState{
    59  					Identity:       "user:someone@google.com",
    60  					IdentityGroups: []string{"googlers", "luci-analysis-access"},
    61  				})
    62  				req := &pb.QueryChangepointGroupSummariesRequest{}
    63  
    64  				res, err := server.QueryChangepointGroupSummaries(ctx, req)
    65  				So(err, ShouldNotBeNil)
    66  				So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
    67  				So(res, ShouldBeNil)
    68  			})
    69  			Convey("e2e", func() {
    70  				ctx = auth.WithState(ctx, &authtest.FakeState{
    71  					Identity:       "user:someone@google.com",
    72  					IdentityGroups: []string{"googlers", "luci-analysis-access"},
    73  				})
    74  				cp1 := makeChangepointRow(1, 2, 4)
    75  				cp2 := makeChangepointRow(2, 2, 3)
    76  				client.ReadChangepointsResult = []*changepoints.ChangepointRow{cp1, cp2}
    77  				stats := &pb.ChangepointGroupStatistics{
    78  					UnexpectedVerdictRateBefore: &pb.ChangepointGroupStatistics_RateDistribution{
    79  						Buckets: &pb.ChangepointGroupStatistics_RateDistribution_RateBuckets{},
    80  					},
    81  					UnexpectedVerdictRateAfter: &pb.ChangepointGroupStatistics_RateDistribution{
    82  						Buckets: &pb.ChangepointGroupStatistics_RateDistribution_RateBuckets{},
    83  					},
    84  					UnexpectedVerdictRateCurrent: &pb.ChangepointGroupStatistics_RateDistribution{
    85  						Buckets: &pb.ChangepointGroupStatistics_RateDistribution_RateBuckets{},
    86  					},
    87  					UnexpectedVerdictRateChange: &pb.ChangepointGroupStatistics_RateChangeBuckets{},
    88  				}
    89  				changepointGroupSummary := &pb.ChangepointGroupSummary{
    90  					CanonicalChangepoint: &pb.Changepoint{
    91  						Project:     "chromium",
    92  						TestId:      "test1",
    93  						VariantHash: "5097aaaaaaaaaaaa",
    94  						Variant: &pb.Variant{
    95  							Def: map[string]string{
    96  								"var":  "abc",
    97  								"varr": "xyx",
    98  							},
    99  						},
   100  						RefHash: "b920ffffffffffff",
   101  						Ref: &pb.SourceRef{
   102  							System: &pb.SourceRef_Gitiles{
   103  								Gitiles: &pb.GitilesRef{
   104  									Host:    "host",
   105  									Project: "project",
   106  									Ref:     "ref",
   107  								},
   108  							},
   109  						},
   110  						StartHour:                    timestamppb.New(time.Unix(1000, 0)),
   111  						StartPositionLowerBound_99Th: cp1.LowerBound99th,
   112  						StartPositionUpperBound_99Th: cp1.UpperBound99th,
   113  						NominalStartPosition:         cp1.NominalStartPosition,
   114  					},
   115  					Statistics: stats,
   116  				}
   117  				Convey("with no predicates", func() {
   118  					req := &pb.QueryChangepointGroupSummariesRequest{Project: "chromium"}
   119  
   120  					res, err := server.QueryChangepointGroupSummaries(ctx, req)
   121  					So(err, ShouldBeNil)
   122  					stats.Count = 2
   123  					stats.UnexpectedVerdictRateBefore.Average = 0.3
   124  					stats.UnexpectedVerdictRateBefore.Buckets.CountAbove_5LessThan_95Percent = 2
   125  					stats.UnexpectedVerdictRateAfter.Average = 0.99
   126  					stats.UnexpectedVerdictRateAfter.Buckets.CountAbove_95Percent = 2
   127  					stats.UnexpectedVerdictRateCurrent.Average = 0
   128  					stats.UnexpectedVerdictRateCurrent.Buckets.CountLess_5Percent = 2
   129  					stats.UnexpectedVerdictRateChange.CountIncreased_50To_100Percent = 2
   130  					changepointGroupSummary.Statistics = stats
   131  					So(res, ShouldResembleProto, &pb.QueryChangepointGroupSummariesResponse{
   132  						GroupSummaries: []*pb.ChangepointGroupSummary{changepointGroupSummary},
   133  					})
   134  				})
   135  				Convey("with predicates", func() {
   136  					Convey("test id prefix predicate", func() {
   137  						req := &pb.QueryChangepointGroupSummariesRequest{
   138  							Project: "chromium",
   139  							Predicate: &pb.ChangepointPredicate{
   140  								TestIdPrefix: "test2",
   141  							}}
   142  
   143  						res, err := server.QueryChangepointGroupSummaries(ctx, req)
   144  						So(err, ShouldBeNil)
   145  						stats.Count = 1
   146  						stats.UnexpectedVerdictRateBefore.Average = 0.3
   147  						stats.UnexpectedVerdictRateBefore.Buckets.CountAbove_5LessThan_95Percent = 1
   148  						stats.UnexpectedVerdictRateAfter.Average = 0.99
   149  						stats.UnexpectedVerdictRateAfter.Buckets.CountAbove_95Percent = 1
   150  						stats.UnexpectedVerdictRateCurrent.Average = 0
   151  						stats.UnexpectedVerdictRateCurrent.Buckets.CountLess_5Percent = 1
   152  						stats.UnexpectedVerdictRateChange.CountIncreased_50To_100Percent = 1
   153  						changepointGroupSummary.Statistics = stats
   154  						changepointGroupSummary.CanonicalChangepoint.TestId = "test2"
   155  						changepointGroupSummary.CanonicalChangepoint.NominalStartPosition = 2
   156  						changepointGroupSummary.CanonicalChangepoint.StartPositionUpperBound_99Th = 3
   157  						So(res, ShouldResembleProto, &pb.QueryChangepointGroupSummariesResponse{
   158  							GroupSummaries: []*pb.ChangepointGroupSummary{changepointGroupSummary},
   159  						})
   160  					})
   161  					Convey("failure rate change predicate", func() {
   162  						req := &pb.QueryChangepointGroupSummariesRequest{
   163  							Project: "chromium",
   164  							Predicate: &pb.ChangepointPredicate{
   165  								UnexpectedVerdictRateChangeRange: &pb.NumericRange{
   166  									LowerBound: 0.7,
   167  									UpperBound: 1,
   168  								},
   169  							}}
   170  
   171  						res, err := server.QueryChangepointGroupSummaries(ctx, req)
   172  						So(err, ShouldBeNil)
   173  						So(res, ShouldResembleProto, &pb.QueryChangepointGroupSummariesResponse{})
   174  					})
   175  				})
   176  			})
   177  		})
   178  
   179  		Convey("QueryChangepointsInGroup", func() {
   180  			Convey("unauthorised requests are rejected", func() {
   181  				req := &pb.QueryChangepointsInGroupRequest{
   182  					Project: "chromium",
   183  				}
   184  
   185  				res, err := server.QueryChangepointsInGroup(ctx, req)
   186  				So(err, ShouldErrLike, `not a member of googlers`)
   187  				So(err, ShouldHaveGRPCStatus, codes.PermissionDenied)
   188  				So(res, ShouldBeNil)
   189  			})
   190  			Convey("invalid requests are rejected", func() {
   191  				ctx = auth.WithState(ctx, &authtest.FakeState{
   192  					Identity:       "user:someone@google.com",
   193  					IdentityGroups: []string{"googlers", "luci-analysis-access"},
   194  				})
   195  				req := &pb.QueryChangepointsInGroupRequest{}
   196  
   197  				res, err := server.QueryChangepointsInGroup(ctx, req)
   198  				So(err, ShouldNotBeNil)
   199  				So(err, ShouldHaveGRPCStatus, codes.InvalidArgument)
   200  				So(res, ShouldBeNil)
   201  			})
   202  
   203  			Convey("e2e", func() {
   204  				ctx = auth.WithState(ctx, &authtest.FakeState{
   205  					Identity:       "user:someone@google.com",
   206  					IdentityGroups: []string{"googlers", "luci-analysis-access"},
   207  				})
   208  				// Group1.
   209  				cp1 := makeChangepointRow(1, 2, 4)
   210  				cp2 := makeChangepointRow(2, 2, 3)
   211  				// Group2.
   212  				cp3 := makeChangepointRow(1, 2, 20)
   213  				cp4 := makeChangepointRow(2, 2, 20)
   214  				// Group3.
   215  				cp5 := makeChangepointRow(1, 20, 40)
   216  				cp6 := makeChangepointRow(2, 20, 30)
   217  				client.ReadChangepointsResult = []*changepoints.ChangepointRow{cp1, cp2, cp3, cp4, cp5, cp6}
   218  				req := &pb.QueryChangepointsInGroupRequest{
   219  					Project: "chromium",
   220  					GroupKey: &pb.QueryChangepointsInGroupRequest_ChangepointIdentifier{
   221  						TestId:               "test2",
   222  						VariantHash:          "5097aaaaaaaaaaaa",
   223  						RefHash:              "b920ffffffffffff",
   224  						NominalStartPosition: 20, // Match group 3.
   225  						StartHour:            timestamppb.New(time.Unix(100, 0)),
   226  					},
   227  				}
   228  
   229  				Convey("group found", func() {
   230  					Convey("with no predicates", func() {
   231  						res, err := server.QueryChangepointsInGroup(ctx, req)
   232  						So(err, ShouldBeNil)
   233  						So(res.Changepoints, ShouldHaveLength, 2)
   234  						So(res.Changepoints[0].TestId, ShouldEqual, "test1")
   235  						So(res.Changepoints[0].NominalStartPosition, ShouldEqual, cp5.NominalStartPosition)
   236  						So(res.Changepoints[1].TestId, ShouldEqual, "test2")
   237  						So(res.Changepoints[1].NominalStartPosition, ShouldEqual, cp6.NominalStartPosition)
   238  					})
   239  
   240  					Convey("with predicates", func() {
   241  						req.Predicate = &pb.ChangepointPredicate{
   242  							TestIdPrefix: "test2",
   243  						}
   244  
   245  						res, err := server.QueryChangepointsInGroup(ctx, req)
   246  						So(err, ShouldBeNil)
   247  						So(res.Changepoints, ShouldHaveLength, 1)
   248  						So(res.Changepoints[0].TestId, ShouldEqual, "test2")
   249  						So(res.Changepoints[0].NominalStartPosition, ShouldEqual, cp6.NominalStartPosition)
   250  					})
   251  				})
   252  
   253  				Convey("group not found", func() {
   254  					req.GroupKey.NominalStartPosition = 100 // no match.
   255  
   256  					res, err := server.QueryChangepointsInGroup(ctx, req)
   257  					So(err, ShouldHaveGRPCStatus, codes.NotFound)
   258  					So(res, ShouldBeNil)
   259  				})
   260  			})
   261  		})
   262  	})
   263  }
   264  
   265  func TestValidateRequest(t *testing.T) {
   266  	t.Parallel()
   267  
   268  	Convey("validateQueryChangepointGroupSummariesRequest", t, func() {
   269  		req := &pb.QueryChangepointGroupSummariesRequest{
   270  			Project: "chromium",
   271  			Predicate: &pb.ChangepointPredicate{
   272  				TestIdPrefix: "test",
   273  				UnexpectedVerdictRateChangeRange: &pb.NumericRange{
   274  					LowerBound: 0,
   275  					UpperBound: 1,
   276  				},
   277  			},
   278  		}
   279  		Convey("valid", func() {
   280  			err := validateQueryChangepointGroupSummariesRequest(req)
   281  			So(err, ShouldBeNil)
   282  		})
   283  		Convey("no project", func() {
   284  			req.Project = ""
   285  			err := validateQueryChangepointGroupSummariesRequest(req)
   286  			So(err, ShouldErrLike, "project: unspecified")
   287  		})
   288  		Convey("invalid predicate", func() {
   289  			req.Predicate.TestIdPrefix = "\xFF"
   290  			err := validateQueryChangepointGroupSummariesRequest(req)
   291  			So(err, ShouldErrLike, "test_id_prefix: not a valid utf8 string")
   292  		})
   293  	})
   294  
   295  	Convey("validateQueryChangepointsInGroupRequest", t, func() {
   296  		req := &pb.QueryChangepointsInGroupRequest{
   297  			Project: "chromium",
   298  			GroupKey: &pb.QueryChangepointsInGroupRequest_ChangepointIdentifier{
   299  				TestId:               "testid",
   300  				VariantHash:          "5097aaaaaaaaaaaa",
   301  				RefHash:              "b920ffffffffffff",
   302  				NominalStartPosition: 1,
   303  				StartHour:            timestamppb.New(time.Unix(1000, 0)),
   304  			},
   305  			Predicate: &pb.ChangepointPredicate{},
   306  		}
   307  		Convey("valid", func() {
   308  			err := validateQueryChangepointsInGroupRequest(req)
   309  			So(err, ShouldBeNil)
   310  		})
   311  		Convey("no project", func() {
   312  			req.Project = ""
   313  			err := validateQueryChangepointsInGroupRequest(req)
   314  			So(err, ShouldErrLike, "project: unspecified")
   315  		})
   316  		Convey("no group key", func() {
   317  			req.GroupKey = nil
   318  			err := validateQueryChangepointsInGroupRequest(req)
   319  			So(err, ShouldErrLike, "group_key: unspecified")
   320  		})
   321  		Convey("invalid group key", func() {
   322  			req.GroupKey.TestId = "\xFF"
   323  			err := validateQueryChangepointsInGroupRequest(req)
   324  			So(err, ShouldErrLike, "test_id: not a valid utf8 string")
   325  		})
   326  	})
   327  
   328  	Convey("validateChangepointPredicate", t, func() {
   329  		Convey("invalid test prefix", func() {
   330  			predicate := &pb.ChangepointPredicate{
   331  				TestIdPrefix: "\xFF",
   332  			}
   333  			err := validateChangepointPredicate(predicate)
   334  			So(err, ShouldErrLike, "test_id_prefix: not a valid utf8 string")
   335  		})
   336  		Convey("invalid lower bound", func() {
   337  			predicate := &pb.ChangepointPredicate{
   338  				UnexpectedVerdictRateChangeRange: &pb.NumericRange{
   339  					LowerBound: 2,
   340  				},
   341  			}
   342  			err := validateChangepointPredicate(predicate)
   343  			So(err, ShouldErrLike, "unexpected_verdict_rate_change_range_range: lower_bound: should between 0 and 1")
   344  		})
   345  		Convey("invalid upper bound", func() {
   346  			predicate := &pb.ChangepointPredicate{
   347  				UnexpectedVerdictRateChangeRange: &pb.NumericRange{
   348  					UpperBound: 2,
   349  				},
   350  			}
   351  			err := validateChangepointPredicate(predicate)
   352  			So(err, ShouldErrLike, "unexpected_verdict_rate_change_range_range: upper_bound:  should between 0 and 1")
   353  		})
   354  		Convey("upper bound smaller than lower bound", func() {
   355  			predicate := &pb.ChangepointPredicate{
   356  				UnexpectedVerdictRateChangeRange: &pb.NumericRange{
   357  					UpperBound: 0.1,
   358  					LowerBound: 0.2,
   359  				},
   360  			}
   361  			err := validateChangepointPredicate(predicate)
   362  			So(err, ShouldErrLike, "unexpected_verdict_rate_change_range_range: upper_bound must greater or equal to lower_bound")
   363  		})
   364  	})
   365  }
   366  
   367  func makeChangepointRow(TestIDNum, lowerBound, upperBound int64) *changepoints.ChangepointRow {
   368  	return &changepoints.ChangepointRow{
   369  		Project:     "chromium",
   370  		TestIDNum:   TestIDNum,
   371  		TestID:      fmt.Sprintf("test%d", TestIDNum),
   372  		VariantHash: "5097aaaaaaaaaaaa",
   373  		Variant: bigquery.NullJSON{
   374  			JSONVal: "{\"var\":\"abc\",\"varr\":\"xyx\"}",
   375  			Valid:   true,
   376  		},
   377  		Ref: &changepoints.Ref{
   378  			Gitiles: &changepoints.Gitiles{
   379  				Host:    bigquery.NullString{Valid: true, StringVal: "host"},
   380  				Project: bigquery.NullString{Valid: true, StringVal: "project"},
   381  				Ref:     bigquery.NullString{Valid: true, StringVal: "ref"},
   382  			},
   383  		},
   384  		RefHash:                      "b920ffffffffffff",
   385  		UnexpectedVerdictRateCurrent: 0,
   386  		UnexpectedVerdictRateAfter:   0.99,
   387  		UnexpectedVerdictRateBefore:  0.3,
   388  		StartHour:                    time.Unix(1000, 0),
   389  		LowerBound99th:               lowerBound,
   390  		UpperBound99th:               upperBound,
   391  		NominalStartPosition:         (lowerBound + upperBound) / 2,
   392  	}
   393  }
   394  
   395  type fakeChangepointClient struct {
   396  	ReadChangepointsResult []*changepoints.ChangepointRow
   397  }
   398  
   399  func (f *fakeChangepointClient) ReadChangepoints(ctx context.Context, project string, week time.Time) ([]*changepoints.ChangepointRow, error) {
   400  	return f.ReadChangepointsResult, nil
   401  }