go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/bq_test.go (about) 1 // Copyright 2022 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 tasks 16 17 import ( 18 "context" 19 "testing" 20 21 "cloud.google.com/go/bigquery" 22 "google.golang.org/protobuf/types/known/durationpb" 23 "google.golang.org/protobuf/types/known/structpb" 24 25 "go.chromium.org/luci/common/clock/testclock" 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/retry/transient" 28 "go.chromium.org/luci/gae/filter/txndefer" 29 "go.chromium.org/luci/gae/impl/memory" 30 "go.chromium.org/luci/gae/service/datastore" 31 "go.chromium.org/luci/server/tq" 32 33 "go.chromium.org/luci/buildbucket/appengine/internal/clients" 34 "go.chromium.org/luci/buildbucket/appengine/internal/metrics" 35 "go.chromium.org/luci/buildbucket/appengine/model" 36 pb "go.chromium.org/luci/buildbucket/proto" 37 38 . "github.com/smartystreets/goconvey/convey" 39 40 . "go.chromium.org/luci/common/testing/assertions" 41 ) 42 43 func TestBQ(t *testing.T) { 44 t.Parallel() 45 46 Convey("ExportBuild", t, func() { 47 ctx := txndefer.FilterRDS(memory.Use(context.Background())) 48 ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins") 49 datastore.GetTestable(ctx).AutoIndex(true) 50 datastore.GetTestable(ctx).Consistent(true) 51 now := testclock.TestRecentTimeLocal 52 ctx, _ = testclock.UseTime(ctx, now) 53 fakeBq := &clients.FakeBqClient{} 54 ctx = clients.WithBqClient(ctx, fakeBq) 55 b := &model.Build{ 56 ID: 123, 57 Proto: &pb.Build{ 58 Id: 123, 59 Builder: &pb.BuilderID{ 60 Project: "project", 61 Bucket: "bucket", 62 Builder: "builder", 63 }, 64 Status: pb.Status_CANCELED, 65 }, 66 } 67 bk := datastore.KeyForObj(ctx, b) 68 bs := &model.BuildSteps{ID: 1, Build: bk} 69 So(bs.FromProto([]*pb.Step{ 70 { 71 Name: "step", 72 SummaryMarkdown: "summary", 73 Logs: []*pb.Log{{ 74 Name: "log1", 75 Url: "url", 76 ViewUrl: "view_url", 77 }, 78 }, 79 }, 80 }), ShouldBeNil) 81 bi := &model.BuildInfra{ 82 ID: 1, 83 Build: bk, 84 Proto: &pb.BuildInfra{ 85 Backend: &pb.BuildInfra_Backend{ 86 Task: &pb.Task{ 87 Id: &pb.TaskID{ 88 Id: "s93k0402js90", 89 Target: "swarming://chromium-swarm", 90 }, 91 Status: pb.Status_CANCELED, 92 Link: "www.google.com/404", 93 UpdateId: 1100, 94 }, 95 }, 96 Buildbucket: &pb.BuildInfra_Buildbucket{ 97 Hostname: "hostname", 98 }, 99 }, 100 } 101 So(datastore.Put(ctx, b, bi, bs), ShouldBeNil) 102 103 Convey("build not found", func() { 104 err := ExportBuild(ctx, 111) 105 So(tq.Fatal.In(err), ShouldBeTrue) 106 So(err, ShouldErrLike, "build 111 not found when exporting into BQ") 107 }) 108 109 Convey("bad row", func() { 110 ctx1 := context.WithValue(ctx, &clients.FakeBqErrCtxKey, bigquery.PutMultiError{bigquery.RowInsertionError{}}) 111 err := ExportBuild(ctx1, 123) 112 So(err, ShouldErrLike, "bad row for build 123") 113 So(tq.Fatal.In(err), ShouldBeTrue) 114 }) 115 116 Convey("transient BQ err", func() { 117 ctx1 := context.WithValue(ctx, &clients.FakeBqErrCtxKey, errors.New("transient")) 118 err := ExportBuild(ctx1, 123) 119 So(err, ShouldErrLike, "transient error when inserting BQ for build 123") 120 So(transient.Tag.In(err), ShouldBeTrue) 121 }) 122 123 Convey("output properties too large", func() { 124 originLimit := maxBuildSizeInBQ 125 maxBuildSizeInBQ = 10 126 defer func() { 127 maxBuildSizeInBQ = originLimit 128 }() 129 bo := &model.BuildOutputProperties{ 130 Build: bk, 131 Proto: &structpb.Struct{ 132 Fields: map[string]*structpb.Value{ 133 "output": { 134 Kind: &structpb.Value_StringValue{ 135 StringValue: "output value", 136 }, 137 }, 138 }, 139 }, 140 } 141 So(datastore.Put(ctx, bo), ShouldBeNil) 142 143 So(ExportBuild(ctx, 123), ShouldBeNil) 144 rows := fakeBq.GetRows("raw", "completed_builds") 145 So(len(rows), ShouldEqual, 1) 146 So(rows[0].InsertID, ShouldEqual, "123") 147 p, _ := rows[0].Message.(*pb.Build) 148 So(p, ShouldResembleProto, &pb.Build{ 149 Id: 123, 150 Builder: &pb.BuilderID{ 151 Project: "project", 152 Bucket: "bucket", 153 Builder: "builder", 154 }, 155 Status: pb.Status_CANCELED, 156 Steps: []*pb.Step{{ 157 Name: "step", 158 Logs: []*pb.Log{{Name: "log1"}}, 159 }}, 160 Infra: &pb.BuildInfra{ 161 Backend: &pb.BuildInfra_Backend{ 162 Task: &pb.Task{ 163 Id: &pb.TaskID{ 164 Id: "s93k0402js90", 165 Target: "swarming://chromium-swarm", 166 }, 167 Status: pb.Status_CANCELED, 168 Link: "www.google.com/404", 169 }, 170 }, 171 Buildbucket: &pb.BuildInfra_Buildbucket{}, 172 Swarming: &pb.BuildInfra_Swarming{ 173 TaskId: "s93k0402js90", 174 }, 175 }, 176 Input: &pb.Build_Input{}, 177 Output: &pb.Build_Output{ 178 Properties: &structpb.Struct{ 179 Fields: map[string]*structpb.Value{ 180 "strip_reason": { 181 Kind: &structpb.Value_StringValue{ 182 StringValue: "output properties is stripped because it's too large which makes the whole build larger than BQ limit(10MB)", 183 }, 184 }, 185 }, 186 }, 187 }, 188 }) 189 }) 190 191 Convey("summary markdown and cancelation reason are concatenated", func() { 192 b.Proto.SummaryMarkdown = "summary" 193 b.Proto.CancellationMarkdown = "cancelled" 194 So(datastore.Put(ctx, b), ShouldBeNil) 195 196 So(ExportBuild(ctx, 123), ShouldBeNil) 197 rows := fakeBq.GetRows("raw", "completed_builds") 198 So(len(rows), ShouldEqual, 1) 199 So(rows[0].InsertID, ShouldEqual, "123") 200 p, _ := rows[0].Message.(*pb.Build) 201 So(p, ShouldResembleProto, &pb.Build{ 202 Id: 123, 203 Builder: &pb.BuilderID{ 204 Project: "project", 205 Bucket: "bucket", 206 Builder: "builder", 207 }, 208 Status: pb.Status_CANCELED, 209 SummaryMarkdown: "summary\ncancelled", 210 CancellationMarkdown: "cancelled", 211 Steps: []*pb.Step{{ 212 Name: "step", 213 Logs: []*pb.Log{{Name: "log1"}}, 214 }}, 215 Infra: &pb.BuildInfra{ 216 Backend: &pb.BuildInfra_Backend{ 217 Task: &pb.Task{ 218 Id: &pb.TaskID{ 219 Id: "s93k0402js90", 220 Target: "swarming://chromium-swarm", 221 }, 222 Status: pb.Status_CANCELED, 223 Link: "www.google.com/404", 224 }, 225 }, 226 Buildbucket: &pb.BuildInfra_Buildbucket{}, 227 Swarming: &pb.BuildInfra_Swarming{ 228 TaskId: "s93k0402js90", 229 }, 230 }, 231 Input: &pb.Build_Input{}, 232 Output: &pb.Build_Output{}, 233 }) 234 }) 235 236 Convey("success", func() { 237 b.Proto.CancellationMarkdown = "cancelled" 238 So(datastore.Put(ctx, b), ShouldBeNil) 239 240 So(ExportBuild(ctx, 123), ShouldBeNil) 241 rows := fakeBq.GetRows("raw", "completed_builds") 242 So(len(rows), ShouldEqual, 1) 243 So(rows[0].InsertID, ShouldEqual, "123") 244 p, _ := rows[0].Message.(*pb.Build) 245 So(p, ShouldResembleProto, &pb.Build{ 246 Id: 123, 247 Builder: &pb.BuilderID{ 248 Project: "project", 249 Bucket: "bucket", 250 Builder: "builder", 251 }, 252 Status: pb.Status_CANCELED, 253 SummaryMarkdown: "cancelled", 254 CancellationMarkdown: "cancelled", 255 Steps: []*pb.Step{{ 256 Name: "step", 257 Logs: []*pb.Log{{Name: "log1"}}, 258 }}, 259 Infra: &pb.BuildInfra{ 260 Backend: &pb.BuildInfra_Backend{ 261 Task: &pb.Task{ 262 Id: &pb.TaskID{ 263 Id: "s93k0402js90", 264 Target: "swarming://chromium-swarm", 265 }, 266 Status: pb.Status_CANCELED, 267 Link: "www.google.com/404", 268 }, 269 }, 270 Buildbucket: &pb.BuildInfra_Buildbucket{}, 271 Swarming: &pb.BuildInfra_Swarming{ 272 TaskId: "s93k0402js90", 273 }, 274 }, 275 Input: &pb.Build_Input{}, 276 Output: &pb.Build_Output{}, 277 }) 278 }) 279 }) 280 } 281 282 func TestTryBackfillSwarming(t *testing.T) { 283 t.Parallel() 284 285 Convey("tryBackfillSwarming", t, func() { 286 b := &pb.Build{ 287 Id: 1, 288 Builder: &pb.BuilderID{ 289 Project: "project", 290 Bucket: "bucket", 291 Builder: "builder", 292 }, 293 Status: pb.Status_SUCCESS, 294 Infra: &pb.BuildInfra{}, 295 } 296 Convey("noop", func() { 297 Convey("no backend", func() { 298 So(tryBackfillSwarming(b), ShouldBeNil) 299 So(b.Infra.Swarming, ShouldBeNil) 300 }) 301 302 Convey("no backend task", func() { 303 b.Infra.Backend = &pb.BuildInfra_Backend{ 304 Task: &pb.Task{ 305 Id: &pb.TaskID{ 306 Target: "swarming://chromium-swarm", 307 }, 308 }, 309 } 310 So(tryBackfillSwarming(b), ShouldBeNil) 311 So(b.Infra.Swarming, ShouldBeNil) 312 }) 313 314 Convey("not a swarming implemented backend", func() { 315 b.Infra.Backend = &pb.BuildInfra_Backend{ 316 Task: &pb.Task{ 317 Id: &pb.TaskID{ 318 Id: "s93k0402js90", 319 Target: "other://chromium-swarm", 320 }, 321 }, 322 } 323 So(tryBackfillSwarming(b), ShouldBeNil) 324 So(b.Infra.Swarming, ShouldBeNil) 325 }) 326 }) 327 328 Convey("swarming backfilled", func() { 329 taskDims := []*pb.RequestedDimension{ 330 { 331 Key: "key", 332 Value: "value", 333 }, 334 } 335 b.Infra.Backend = &pb.BuildInfra_Backend{ 336 Task: &pb.Task{ 337 Id: &pb.TaskID{ 338 Id: "s93k0402js90", 339 Target: "swarming://chromium-swarm", 340 }, 341 Status: pb.Status_SUCCESS, 342 }, 343 Hostname: "chromium-swarm.appspot.com", 344 Caches: []*pb.CacheEntry{ 345 { 346 Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2", 347 Path: "builder", 348 WaitForWarmCache: &durationpb.Duration{ 349 Seconds: 240, 350 }, 351 }, 352 }, 353 Config: &structpb.Struct{ 354 Fields: map[string]*structpb.Value{ 355 "priority": &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 20}}, 356 "service_account": &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}, 357 }, 358 }, 359 TaskDimensions: taskDims, 360 } 361 Convey("partially fail", func() { 362 b.Infra.Backend.Task.Details = &structpb.Struct{ 363 Fields: map[string]*structpb.Value{ 364 "bot_dimensions": &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "wrong format"}}, 365 }, 366 } 367 So(tryBackfillSwarming(b), ShouldErrLike, "failed to unmarshal task details JSON for build 1") 368 So(b.Infra.Swarming.BotDimensions, ShouldHaveLength, 0) 369 }) 370 371 Convey("pass", func() { 372 b.Infra.Backend.Task.Details = &structpb.Struct{ 373 Fields: map[string]*structpb.Value{ 374 "bot_dimensions": &structpb.Value{ 375 Kind: &structpb.Value_StructValue{ 376 StructValue: &structpb.Struct{ 377 Fields: map[string]*structpb.Value{ 378 "cpu": &structpb.Value{ 379 Kind: &structpb.Value_ListValue{ 380 ListValue: &structpb.ListValue{ 381 Values: []*structpb.Value{ 382 &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "x86"}}, 383 &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "x86-64"}}, 384 }, 385 }, 386 }, 387 }, 388 "os": &structpb.Value{ 389 Kind: &structpb.Value_ListValue{ 390 ListValue: &structpb.ListValue{ 391 Values: []*structpb.Value{ 392 &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "Linux"}}, 393 }, 394 }, 395 }, 396 }, 397 }, 398 }, 399 }, 400 }, 401 }, 402 } 403 expected := &pb.BuildInfra_Swarming{ 404 Hostname: "chromium-swarm.appspot.com", 405 TaskId: "s93k0402js90", 406 Caches: []*pb.BuildInfra_Swarming_CacheEntry{ 407 { 408 Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2", 409 Path: "builder", 410 WaitForWarmCache: &durationpb.Duration{ 411 Seconds: 240, 412 }, 413 }, 414 }, 415 TaskDimensions: taskDims, 416 Priority: int32(20), 417 TaskServiceAccount: "account", 418 BotDimensions: []*pb.StringPair{ 419 { 420 Key: "cpu", 421 Value: "x86", 422 }, 423 { 424 Key: "cpu", 425 Value: "x86-64", 426 }, 427 { 428 Key: "os", 429 Value: "Linux", 430 }, 431 }, 432 } 433 So(tryBackfillSwarming(b), ShouldBeNil) 434 So(b.Infra.Swarming, ShouldResembleProto, expected) 435 }) 436 }) 437 }) 438 } 439 440 func TestTryBackfillBackend(t *testing.T) { 441 t.Parallel() 442 443 Convey("tryBackfillBackend", t, func() { 444 b := &pb.Build{ 445 Id: 1, 446 Builder: &pb.BuilderID{ 447 Project: "project", 448 Bucket: "bucket", 449 Builder: "builder", 450 }, 451 Status: pb.Status_SUCCESS, 452 Infra: &pb.BuildInfra{}, 453 } 454 Convey("noop", func() { 455 Convey("no swarming", func() { 456 So(tryBackfillBackend(b), ShouldBeNil) 457 So(b.Infra.Backend, ShouldBeNil) 458 }) 459 460 Convey("no swarming task", func() { 461 b.Infra.Swarming = &pb.BuildInfra_Swarming{ 462 Hostname: "host", 463 } 464 So(tryBackfillBackend(b), ShouldBeNil) 465 So(b.Infra.Backend, ShouldBeNil) 466 }) 467 }) 468 469 Convey("backend backfilled", func() { 470 taskDims := []*pb.RequestedDimension{ 471 { 472 Key: "key", 473 Value: "value", 474 }, 475 } 476 b.Infra.Swarming = &pb.BuildInfra_Swarming{ 477 Hostname: "chromium-swarm.appspot.com", 478 TaskId: "s93k0402js90", 479 Caches: []*pb.BuildInfra_Swarming_CacheEntry{ 480 { 481 Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2", 482 Path: "builder", 483 WaitForWarmCache: &durationpb.Duration{ 484 Seconds: 240, 485 }, 486 }, 487 }, 488 TaskDimensions: taskDims, 489 Priority: int32(20), 490 TaskServiceAccount: "account", 491 BotDimensions: []*pb.StringPair{ 492 { 493 Key: "cpu", 494 Value: "x86", 495 }, 496 { 497 Key: "cpu", 498 Value: "x86-64", 499 }, 500 { 501 Key: "os", 502 Value: "Linux", 503 }, 504 }, 505 } 506 expected := &pb.BuildInfra_Backend{ 507 Task: &pb.Task{ 508 Id: &pb.TaskID{ 509 Id: "s93k0402js90", 510 Target: "swarming://chromium-swarm", 511 }, 512 }, 513 Hostname: "chromium-swarm.appspot.com", 514 Caches: []*pb.CacheEntry{ 515 { 516 Name: "builder_1809c38861a9996b1748e4640234fbd089992359f6f23f62f68deb98528f5f2b_v2", 517 Path: "builder", 518 WaitForWarmCache: &durationpb.Duration{ 519 Seconds: 240, 520 }, 521 }, 522 }, 523 Config: &structpb.Struct{ 524 Fields: map[string]*structpb.Value{ 525 "priority": &structpb.Value{Kind: &structpb.Value_NumberValue{NumberValue: 20}}, 526 "service_account": &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "account"}}, 527 }, 528 }, 529 TaskDimensions: taskDims, 530 } 531 532 expected.Task.Details = &structpb.Struct{ 533 Fields: map[string]*structpb.Value{ 534 "bot_dimensions": &structpb.Value{ 535 Kind: &structpb.Value_StructValue{ 536 StructValue: &structpb.Struct{ 537 Fields: map[string]*structpb.Value{ 538 "cpu": &structpb.Value{ 539 Kind: &structpb.Value_ListValue{ 540 ListValue: &structpb.ListValue{ 541 Values: []*structpb.Value{ 542 &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "x86"}}, 543 &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "x86-64"}}, 544 }, 545 }, 546 }, 547 }, 548 "os": &structpb.Value{ 549 Kind: &structpb.Value_ListValue{ 550 ListValue: &structpb.ListValue{ 551 Values: []*structpb.Value{ 552 &structpb.Value{Kind: &structpb.Value_StringValue{StringValue: "Linux"}}, 553 }, 554 }, 555 }, 556 }, 557 }, 558 }, 559 }, 560 }, 561 }, 562 } 563 So(tryBackfillBackend(b), ShouldBeNil) 564 So(b.Infra.Backend, ShouldResembleProto, expected) 565 }) 566 }) 567 }