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 }