go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/internal/resultdb/resultdb_test.go (about) 1 // Copyright 2021 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 resultdb 16 17 import ( 18 "context" 19 "crypto/sha256" 20 "encoding/hex" 21 "fmt" 22 "testing" 23 24 "github.com/golang/mock/gomock" 25 "go.chromium.org/luci/gae/impl/memory" 26 "google.golang.org/grpc" 27 "google.golang.org/grpc/codes" 28 "google.golang.org/grpc/metadata" 29 grpcStatus "google.golang.org/grpc/status" 30 "google.golang.org/protobuf/types/known/durationpb" 31 "google.golang.org/protobuf/types/known/timestamppb" 32 33 "go.chromium.org/luci/common/clock/testclock" 34 "go.chromium.org/luci/common/proto" 35 "go.chromium.org/luci/common/retry/transient" 36 "go.chromium.org/luci/gae/service/datastore" 37 rdbPb "go.chromium.org/luci/resultdb/proto/v1" 38 "go.chromium.org/luci/server/tq" 39 40 "go.chromium.org/luci/buildbucket/appengine/model" 41 pb "go.chromium.org/luci/buildbucket/proto" 42 43 . "github.com/smartystreets/goconvey/convey" 44 . "go.chromium.org/luci/common/testing/assertions" 45 ) 46 47 func TestCreateInvocations(t *testing.T) { 48 t.Parallel() 49 50 Convey("create invocations", t, func() { 51 ctl := gomock.NewController(t) 52 defer ctl.Finish() 53 mockClient := rdbPb.NewMockRecorderClient(ctl) 54 ctx := SetMockRecorder(context.Background(), mockClient) 55 ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC) 56 ctx = memory.UseInfo(ctx, "cr-buildbucket-dev") 57 58 bqExports := []*rdbPb.BigQueryExport{} 59 60 Convey("builds without number", func() { 61 builds := []*model.Build{ 62 { 63 ID: 1, 64 Proto: &pb.Build{ 65 Id: 1, 66 Builder: &pb.BuilderID{ 67 Project: "proj1", 68 Bucket: "bucket", 69 Builder: "builder", 70 }, 71 Infra: &pb.BuildInfra{ 72 Resultdb: &pb.BuildInfra_ResultDB{ 73 Hostname: "host", 74 Enable: true, 75 BqExports: bqExports, 76 }, 77 }, 78 }, 79 }, 80 { 81 ID: 2, 82 Proto: &pb.Build{ 83 Id: 2, 84 Builder: &pb.BuilderID{ 85 Project: "proj1", 86 Bucket: "bucket", 87 Builder: "builder", 88 }, 89 Infra: &pb.BuildInfra{ 90 Resultdb: &pb.BuildInfra_ResultDB{ 91 Enable: true, 92 BqExports: bqExports, 93 }, 94 }, 95 }, 96 }, 97 } 98 99 mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual( 100 &rdbPb.CreateInvocationRequest{ 101 InvocationId: "build-1", 102 Invocation: &rdbPb.Invocation{ 103 Deadline: timestamppb.New(testclock.TestRecentTimeUTC), 104 BigqueryExports: bqExports, 105 ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1", 106 Realm: "proj1:bucket", 107 }, 108 RequestId: "build-1", 109 }), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) { 110 h, _ := opt.(grpc.HeaderCallOption) 111 h.HeaderAddr.Set("update-token", "token for build-1") 112 return &rdbPb.Invocation{}, nil 113 }) 114 mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual( 115 &rdbPb.CreateInvocationRequest{ 116 InvocationId: "build-2", 117 Invocation: &rdbPb.Invocation{ 118 Deadline: timestamppb.New(testclock.TestRecentTimeUTC), 119 BigqueryExports: bqExports, 120 ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/2", 121 Realm: "proj1:bucket", 122 }, 123 RequestId: "build-2", 124 }), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) { 125 h, _ := opt.(grpc.HeaderCallOption) 126 h.HeaderAddr.Set("update-token", "token for build-2") 127 return &rdbPb.Invocation{}, nil 128 }) 129 130 err := CreateInvocations(ctx, builds) 131 So(err, ShouldBeNil) 132 So(builds[0].ResultDBUpdateToken, ShouldEqual, "token for build-1") 133 So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-1") 134 So(builds[1].ResultDBUpdateToken, ShouldEqual, "token for build-2") 135 So(builds[1].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-2") 136 }) 137 138 Convey("build with number and expirations", func() { 139 builds := []*model.Build{ 140 { 141 ID: 1, 142 Proto: &pb.Build{ 143 Id: 1, 144 Number: 123, 145 Builder: &pb.BuilderID{ 146 Project: "proj1", 147 Bucket: "bucket", 148 Builder: "builder", 149 }, 150 Infra: &pb.BuildInfra{ 151 Resultdb: &pb.BuildInfra_ResultDB{ 152 Hostname: "host", 153 Enable: true, 154 BqExports: bqExports, 155 }, 156 }, 157 ExecutionTimeout: durationpb.New(1000), 158 SchedulingTimeout: durationpb.New(1000), 159 }, 160 }, 161 } 162 163 deadline := testclock.TestRecentTimeUTC.Add(2000) 164 sha256Bldr := sha256.Sum256([]byte("proj1/bucket/builder")) 165 mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual( 166 &rdbPb.CreateInvocationRequest{ 167 InvocationId: "build-1", 168 Invocation: &rdbPb.Invocation{ 169 Deadline: timestamppb.New(deadline), 170 BigqueryExports: bqExports, 171 ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1", 172 Realm: "proj1:bucket", 173 }, 174 RequestId: "build-1", 175 }), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) { 176 h, _ := opt.(grpc.HeaderCallOption) 177 h.HeaderAddr.Set("update-token", "token for build id 1") 178 return &rdbPb.Invocation{}, nil 179 }) 180 mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual( 181 &rdbPb.CreateInvocationRequest{ 182 InvocationId: fmt.Sprintf("build-%s-123", hex.EncodeToString(sha256Bldr[:])), 183 Invocation: &rdbPb.Invocation{ 184 IncludedInvocations: []string{"invocations/build-1"}, 185 ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1", 186 State: rdbPb.Invocation_FINALIZING, 187 Realm: "proj1:bucket", 188 }, 189 RequestId: "build-1-123", 190 })).Return(&rdbPb.Invocation{}, nil) 191 192 err := CreateInvocations(ctx, builds) 193 So(err, ShouldBeNil) 194 So(len(builds), ShouldEqual, 1) 195 So(builds[0].ResultDBUpdateToken, ShouldEqual, "token for build id 1") 196 So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-1") 197 }) 198 199 Convey("already exists error", func() { 200 builds := []*model.Build{ 201 { 202 ID: 1, 203 Proto: &pb.Build{ 204 Id: 1, 205 Number: 123, 206 Builder: &pb.BuilderID{ 207 Project: "proj1", 208 Bucket: "bucket", 209 Builder: "builder", 210 }, 211 Infra: &pb.BuildInfra{ 212 Resultdb: &pb.BuildInfra_ResultDB{ 213 Hostname: "host", 214 Enable: true, 215 BqExports: bqExports, 216 }, 217 }, 218 }, 219 }, 220 } 221 222 mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual( 223 &rdbPb.CreateInvocationRequest{ 224 InvocationId: "build-1", 225 Invocation: &rdbPb.Invocation{ 226 Deadline: timestamppb.New(testclock.TestRecentTimeUTC), 227 BigqueryExports: bqExports, 228 ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1", 229 Realm: "proj1:bucket", 230 }, 231 RequestId: "build-1", 232 }), gomock.Any()).Return(nil, grpcStatus.Error(codes.AlreadyExists, "already exists")) 233 234 err := CreateInvocations(ctx, builds) 235 So(err, ShouldErrLike, "failed to create the invocation for build id: 1: rpc error: code = AlreadyExists desc = already exists") 236 }) 237 238 Convey("resultDB throws err", func() { 239 builds := []*model.Build{ 240 { 241 ID: 1, 242 Proto: &pb.Build{ 243 Id: 1, 244 Builder: &pb.BuilderID{ 245 Project: "proj1", 246 Bucket: "bucket", 247 Builder: "builder", 248 }, 249 Infra: &pb.BuildInfra{ 250 Resultdb: &pb.BuildInfra_ResultDB{ 251 Hostname: "host", 252 Enable: true, 253 BqExports: bqExports, 254 }, 255 }, 256 }, 257 }, 258 } 259 260 mockClient.EXPECT().CreateInvocation(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, grpcStatus.Error(codes.DeadlineExceeded, "timeout")) 261 262 err := CreateInvocations(ctx, builds) 263 So(err, ShouldErrLike, "failed to create the invocation for build id: 1: rpc error: code = DeadlineExceeded desc = timeout") 264 }) 265 266 Convey("partial success", func() { 267 builds := []*model.Build{ 268 { 269 ID: 1, 270 Proto: &pb.Build{ 271 Id: 1, 272 Builder: &pb.BuilderID{ 273 Project: "proj1", 274 Bucket: "bucket", 275 Builder: "builder", 276 }, 277 Infra: &pb.BuildInfra{ 278 Resultdb: &pb.BuildInfra_ResultDB{ 279 Hostname: "host", 280 Enable: true, 281 BqExports: bqExports, 282 }, 283 }, 284 }, 285 }, 286 { 287 ID: 2, 288 Proto: &pb.Build{ 289 Id: 2, 290 Builder: &pb.BuilderID{ 291 Project: "proj1", 292 Bucket: "bucket", 293 Builder: "builder", 294 }, 295 Infra: &pb.BuildInfra{ 296 Resultdb: &pb.BuildInfra_ResultDB{ 297 Hostname: "host", 298 Enable: true, 299 BqExports: bqExports, 300 }, 301 }, 302 }, 303 }, 304 } 305 306 mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual( 307 &rdbPb.CreateInvocationRequest{ 308 InvocationId: "build-1", 309 Invocation: &rdbPb.Invocation{ 310 Deadline: timestamppb.New(testclock.TestRecentTimeUTC), 311 BigqueryExports: bqExports, 312 ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/1", 313 Realm: "proj1:bucket", 314 }, 315 RequestId: "build-1", 316 }), gomock.Any()).Return(nil, grpcStatus.Error(codes.Internal, "error")) 317 mockClient.EXPECT().CreateInvocation(gomock.Any(), proto.MatcherEqual( 318 &rdbPb.CreateInvocationRequest{ 319 InvocationId: "build-2", 320 Invocation: &rdbPb.Invocation{ 321 Deadline: timestamppb.New(testclock.TestRecentTimeUTC), 322 BigqueryExports: bqExports, 323 ProducerResource: "//cr-buildbucket-dev.appspot.com/builds/2", 324 Realm: "proj1:bucket", 325 }, 326 RequestId: "build-2", 327 }), gomock.Any()).DoAndReturn(func(ctx context.Context, in *rdbPb.CreateInvocationRequest, opt grpc.CallOption) (*rdbPb.Invocation, error) { 328 h, _ := opt.(grpc.HeaderCallOption) 329 h.HeaderAddr.Set("update-token", "update token") 330 return &rdbPb.Invocation{}, nil 331 }) 332 333 err := CreateInvocations(ctx, builds) 334 So(err[0], ShouldErrLike, "failed to create the invocation for build id: 1: rpc error: code = Internal desc = error") 335 So(err[1], ShouldBeNil) 336 So(builds[0].ResultDBUpdateToken, ShouldEqual, "") 337 So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "") 338 So(builds[1].ResultDBUpdateToken, ShouldEqual, "update token") 339 So(builds[1].Proto.Infra.Resultdb.Invocation, ShouldEqual, "invocations/build-2") 340 }) 341 342 Convey("resultDB not enabled", func() { 343 builds := []*model.Build{ 344 { 345 ID: 1, 346 Proto: &pb.Build{ 347 Id: 1, 348 Builder: &pb.BuilderID{ 349 Project: "proj1", 350 Bucket: "bucket", 351 Builder: "builder", 352 }, 353 Infra: &pb.BuildInfra{Resultdb: &pb.BuildInfra_ResultDB{ 354 Hostname: "host", 355 Enable: false, 356 }}, 357 }, 358 }, 359 } 360 361 err := CreateInvocations(ctx, builds) 362 So(err, ShouldBeNil) 363 So(builds[0].Proto.Infra.Resultdb.Invocation, ShouldEqual, "") 364 }) 365 }) 366 } 367 368 func TestFinalizeInvocation(t *testing.T) { 369 t.Parallel() 370 371 Convey("finalize invocations", t, func() { 372 ctl := gomock.NewController(t) 373 defer ctl.Finish() 374 mockClient := rdbPb.NewMockRecorderClient(ctl) 375 ctx := memory.Use(context.Background()) 376 ctx = SetMockRecorder(ctx, mockClient) 377 datastore.GetTestable(ctx).AutoIndex(true) 378 datastore.GetTestable(ctx).Consistent(true) 379 380 So(datastore.Put(ctx, &model.Build{ 381 ID: 1, 382 Project: "project", 383 BucketID: "bucket", 384 BuilderID: "builder", 385 ResultDBUpdateToken: "token", 386 Proto: &pb.Build{ 387 Id: 1, 388 Builder: &pb.BuilderID{ 389 Project: "project", 390 Bucket: "bucket", 391 Builder: "builder", 392 }, 393 Status: pb.Status_SUCCESS, 394 }, 395 }), ShouldBeNil) 396 397 Convey("no exists", func() { 398 So(FinalizeInvocation(ctx, 1), ShouldErrLike, "build 1 or buildInfra not found") 399 }) 400 401 Convey("no resultdb hostname", func() { 402 So(datastore.Put(ctx, &model.BuildInfra{ 403 ID: 1, 404 Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}), 405 Proto: &pb.BuildInfra{ 406 Resultdb: &pb.BuildInfra_ResultDB{ 407 Invocation: "invocation", 408 }, 409 }, 410 }), ShouldBeNil) 411 412 mockClient.EXPECT().FinalizeInvocation(gomock.Any(), gomock.Any()).Times(0) 413 So(FinalizeInvocation(ctx, 1), ShouldBeNil) 414 }) 415 416 Convey("no invocation", func() { 417 So(datastore.Put(ctx, &model.BuildInfra{ 418 ID: 1, 419 Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}), 420 Proto: &pb.BuildInfra{ 421 Resultdb: &pb.BuildInfra_ResultDB{ 422 Hostname: "hostname", 423 }, 424 }, 425 }), ShouldBeNil) 426 427 mockClient.EXPECT().FinalizeInvocation(gomock.Any(), gomock.Any()).Times(0) 428 So(FinalizeInvocation(ctx, 1), ShouldBeNil) 429 }) 430 431 Convey("success", func() { 432 So(datastore.Put(ctx, &model.BuildInfra{ 433 ID: 1, 434 Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}), 435 Proto: &pb.BuildInfra{ 436 Resultdb: &pb.BuildInfra_ResultDB{ 437 Hostname: "hostname", 438 Invocation: "invocation", 439 }, 440 }, 441 }), ShouldBeNil) 442 443 expectedCtx := metadata.AppendToOutgoingContext(ctx, "update-token", "token") 444 mockClient.EXPECT().FinalizeInvocation(expectedCtx, proto.MatcherEqual(&rdbPb.FinalizeInvocationRequest{ 445 Name: "invocation", 446 })).Return(&rdbPb.Invocation{}, nil) 447 448 So(FinalizeInvocation(ctx, 1), ShouldBeNil) 449 }) 450 451 Convey("resultDB server fatal err", func() { 452 So(datastore.Put(ctx, &model.BuildInfra{ 453 ID: 1, 454 Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}), 455 Proto: &pb.BuildInfra{ 456 Resultdb: &pb.BuildInfra_ResultDB{ 457 Hostname: "hostname", 458 Invocation: "invocation", 459 }, 460 }, 461 }), ShouldBeNil) 462 463 mockClient.EXPECT().FinalizeInvocation(gomock.Any(), proto.MatcherEqual(&rdbPb.FinalizeInvocationRequest{ 464 Name: "invocation", 465 })).Return(nil, grpcStatus.Error(codes.PermissionDenied, "permission denied")) 466 467 err := FinalizeInvocation(ctx, 1) 468 So(err, ShouldNotBeNil) 469 So(tq.Fatal.In(err), ShouldBeTrue) 470 }) 471 472 Convey("resultDB server retryable err", func() { 473 So(datastore.Put(ctx, &model.BuildInfra{ 474 ID: 1, 475 Build: datastore.KeyForObj(ctx, &model.Build{ID: 1}), 476 Proto: &pb.BuildInfra{ 477 Resultdb: &pb.BuildInfra_ResultDB{ 478 Hostname: "hostname", 479 Invocation: "invocation", 480 }, 481 }, 482 }), ShouldBeNil) 483 484 mockClient.EXPECT().FinalizeInvocation(gomock.Any(), proto.MatcherEqual(&rdbPb.FinalizeInvocationRequest{ 485 Name: "invocation", 486 })).Return(nil, grpcStatus.Error(codes.Internal, "internal error")) 487 488 err := FinalizeInvocation(ctx, 1) 489 So(err, ShouldNotBeNil) 490 So(transient.Tag.In(err), ShouldBeTrue) 491 }) 492 }) 493 }