go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/model/details_test.go (about) 1 // Copyright 2020 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 model 16 17 import ( 18 "context" 19 "strconv" 20 "testing" 21 22 "google.golang.org/protobuf/proto" 23 "google.golang.org/protobuf/types/known/structpb" 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/gae/impl/memory" 28 "go.chromium.org/luci/gae/service/datastore" 29 30 pb "go.chromium.org/luci/buildbucket/proto" 31 32 . "github.com/smartystreets/goconvey/convey" 33 34 . "go.chromium.org/luci/common/testing/assertions" 35 ) 36 37 func TestDetails(t *testing.T) { 38 t.Parallel() 39 40 Convey("Details", t, func() { 41 Convey("BuildSteps", func() { 42 ctx := memory.Use(context.Background()) 43 datastore.GetTestable(ctx).AutoIndex(true) 44 datastore.GetTestable(ctx).Consistent(true) 45 46 Convey("CancelIncomplete", func() { 47 now := ×tamppb.Timestamp{ 48 Seconds: 123, 49 } 50 51 Convey("error", func() { 52 s := &BuildSteps{ 53 IsZipped: true, 54 } 55 ch, err := s.CancelIncomplete(ctx, now) 56 So(err, ShouldErrLike, "error creating reader") 57 So(ch, ShouldBeFalse) 58 So(s, ShouldResemble, &BuildSteps{ 59 IsZipped: true, 60 }) 61 }) 62 63 Convey("not changed", func() { 64 Convey("empty", func() { 65 b, err := proto.Marshal(&pb.Build{}) 66 So(err, ShouldBeNil) 67 68 s := &BuildSteps{ 69 IsZipped: false, 70 Bytes: b, 71 } 72 ch, err := s.CancelIncomplete(ctx, now) 73 So(err, ShouldBeNil) 74 So(ch, ShouldBeFalse) 75 So(s, ShouldResemble, &BuildSteps{ 76 IsZipped: false, 77 Bytes: b, 78 }) 79 }) 80 81 Convey("completed", func() { 82 b, err := proto.Marshal(&pb.Build{ 83 Steps: []*pb.Step{ 84 { 85 Status: pb.Status_SUCCESS, 86 }, 87 }, 88 }) 89 So(err, ShouldBeNil) 90 s := &BuildSteps{ 91 IsZipped: false, 92 Bytes: b, 93 } 94 ch, err := s.CancelIncomplete(ctx, now) 95 So(err, ShouldBeNil) 96 So(ch, ShouldBeFalse) 97 So(s, ShouldResemble, &BuildSteps{ 98 IsZipped: false, 99 Bytes: b, 100 }) 101 }) 102 }) 103 104 Convey("changed", func() { 105 b, err := proto.Marshal(&pb.Build{ 106 Steps: []*pb.Step{ 107 { 108 Name: "step", 109 }, 110 }, 111 }) 112 So(err, ShouldBeNil) 113 s := &BuildSteps{ 114 IsZipped: false, 115 Bytes: b, 116 } 117 b, err = proto.Marshal(&pb.Build{ 118 Steps: []*pb.Step{ 119 { 120 EndTime: now, 121 Name: "step", 122 Status: pb.Status_CANCELED, 123 }, 124 }, 125 }) 126 So(err, ShouldBeNil) 127 ch, err := s.CancelIncomplete(ctx, now) 128 So(err, ShouldBeNil) 129 So(ch, ShouldBeTrue) 130 So(s, ShouldResemble, &BuildSteps{ 131 IsZipped: false, 132 Bytes: b, 133 }) 134 }) 135 }) 136 137 Convey("FromProto", func() { 138 Convey("not zipped", func() { 139 b, err := proto.Marshal(&pb.Build{ 140 Steps: []*pb.Step{ 141 { 142 Name: "step", 143 }, 144 }, 145 }) 146 So(err, ShouldBeNil) 147 s := &BuildSteps{} 148 So(s.FromProto([]*pb.Step{ 149 { 150 Name: "step", 151 }, 152 }), ShouldBeNil) 153 So(s.Bytes, ShouldResemble, b) 154 So(s.IsZipped, ShouldBeFalse) 155 }) 156 }) 157 158 Convey("ToProto", func() { 159 Convey("zipped", func() { 160 Convey("error", func() { 161 s := &BuildSteps{ 162 IsZipped: true, 163 } 164 p, err := s.ToProto(ctx) 165 So(err, ShouldErrLike, "error creating reader") 166 So(p, ShouldBeNil) 167 }) 168 169 Convey("ok", func() { 170 s := &BuildSteps{ 171 // { name: "step" } 172 Bytes: []byte{120, 156, 234, 98, 100, 227, 98, 41, 46, 73, 45, 0, 4, 0, 0, 255, 255, 9, 199, 2, 92}, 173 IsZipped: true, 174 } 175 p, err := s.ToProto(ctx) 176 So(err, ShouldBeNil) 177 So(p, ShouldResembleProto, []*pb.Step{ 178 { 179 Name: "step", 180 }, 181 }) 182 }) 183 }) 184 185 Convey("not zipped", func() { 186 b, err := proto.Marshal(&pb.Build{ 187 Steps: []*pb.Step{ 188 { 189 Name: "step", 190 }, 191 }, 192 }) 193 So(err, ShouldBeNil) 194 s := &BuildSteps{ 195 IsZipped: false, 196 Bytes: b, 197 } 198 p, err := s.ToProto(ctx) 199 So(err, ShouldBeNil) 200 So(p, ShouldResembleProto, []*pb.Step{ 201 { 202 Name: "step", 203 }, 204 }) 205 }) 206 }) 207 }) 208 209 Convey("defaultStructValue", func() { 210 Convey("nil struct", func() { 211 defaultStructValues(nil) 212 }) 213 214 Convey("empty struct", func() { 215 s := &structpb.Struct{} 216 defaultStructValues(s) 217 So(s, ShouldResembleProto, &structpb.Struct{}) 218 }) 219 220 Convey("empty fields", func() { 221 s := &structpb.Struct{ 222 Fields: map[string]*structpb.Value{}, 223 } 224 defaultStructValues(s) 225 So(s, ShouldResembleProto, &structpb.Struct{ 226 Fields: map[string]*structpb.Value{}, 227 }) 228 }) 229 230 Convey("nil value", func() { 231 s := &structpb.Struct{ 232 Fields: map[string]*structpb.Value{ 233 "key": nil, 234 }, 235 } 236 defaultStructValues(s) 237 So(s, ShouldResembleProto, &structpb.Struct{ 238 Fields: map[string]*structpb.Value{ 239 "key": { 240 Kind: &structpb.Value_NullValue{}, 241 }, 242 }, 243 }) 244 }) 245 246 Convey("empty value", func() { 247 s := &structpb.Struct{ 248 Fields: map[string]*structpb.Value{ 249 "key": {}, 250 }, 251 } 252 defaultStructValues(s) 253 So(s, ShouldResembleProto, &structpb.Struct{ 254 Fields: map[string]*structpb.Value{ 255 "key": { 256 Kind: &structpb.Value_NullValue{}, 257 }, 258 }, 259 }) 260 }) 261 262 Convey("recursive", func() { 263 s := &structpb.Struct{ 264 Fields: map[string]*structpb.Value{ 265 "key": { 266 Kind: &structpb.Value_StructValue{ 267 StructValue: &structpb.Struct{ 268 Fields: map[string]*structpb.Value{ 269 "key": {}, 270 }, 271 }, 272 }, 273 }, 274 }, 275 } 276 defaultStructValues(s) 277 So(s, ShouldResembleProto, &structpb.Struct{ 278 Fields: map[string]*structpb.Value{ 279 "key": { 280 Kind: &structpb.Value_StructValue{ 281 StructValue: &structpb.Struct{ 282 Fields: map[string]*structpb.Value{ 283 "key": { 284 Kind: &structpb.Value_NullValue{}, 285 }, 286 }, 287 }, 288 }, 289 }, 290 }, 291 }) 292 }) 293 }) 294 }) 295 296 Convey("BuildOutputProperties", t, func() { 297 ctx := memory.Use(context.Background()) 298 datastore.GetTestable(ctx).AutoIndex(true) 299 datastore.GetTestable(ctx).Consistent(true) 300 301 Convey("normal", func() { 302 prop, err := structpb.NewStruct(map[string]any{"key": "value"}) 303 So(err, ShouldBeNil) 304 outProp := &BuildOutputProperties{ 305 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 306 Proto: prop, 307 } 308 So(outProp.Put(ctx), ShouldBeNil) 309 310 count, err := datastore.Count(ctx, datastore.NewQuery("PropertyChunk")) 311 So(err, ShouldBeNil) 312 So(count, ShouldEqual, 0) 313 314 outPropInDB := &BuildOutputProperties{ 315 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 316 } 317 So(outPropInDB.Get(ctx), ShouldBeNil) 318 So(outPropInDB.Proto, ShouldResembleProto, mustStruct(map[string]any{ 319 "key": "value", 320 })) 321 So(outPropInDB.ChunkCount, ShouldEqual, 0) 322 323 Convey("normal -> larger", func() { 324 larger := proto.Clone(prop).(*structpb.Struct) 325 larger.Fields["new_key"] = &structpb.Value{ 326 Kind: &structpb.Value_StringValue{ 327 StringValue: "new_value", 328 }, 329 } 330 331 outProp.Proto = larger 332 So(outProp.Put(ctx), ShouldBeNil) 333 So(outProp.ChunkCount, ShouldEqual, 0) 334 335 outPropInDB := &BuildOutputProperties{ 336 Build: outProp.Build, 337 } 338 So(outPropInDB.Get(ctx), ShouldBeNil) 339 So(outPropInDB.Proto, ShouldResembleProto, mustStruct(map[string]any{ 340 "key": "value", 341 "new_key": "new_value", 342 })) 343 }) 344 345 Convey("normal -> extreme large", func() { 346 larger, err := structpb.NewStruct(map[string]any{}) 347 So(err, ShouldBeNil) 348 k := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_key" 349 v := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_value" 350 for i := 0; i < 10000; i++ { 351 larger.Fields[k+strconv.Itoa(i)] = &structpb.Value{ 352 Kind: &structpb.Value_StringValue{ 353 StringValue: v, 354 }, 355 } 356 } 357 358 outProp.Proto = larger 359 So(outProp.Put(ctx), ShouldBeNil) 360 So(outProp.ChunkCount, ShouldAlmostEqual, 1, 1) 361 362 outPropInDB := &BuildOutputProperties{ 363 Build: outProp.Build, 364 } 365 So(outPropInDB.Get(ctx), ShouldBeNil) 366 So(outPropInDB.Proto, ShouldResembleProto, larger) 367 }) 368 }) 369 370 Convey("large", func() { 371 largeProps, err := structpb.NewStruct(map[string]any{}) 372 So(err, ShouldBeNil) 373 k := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_key" 374 v := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_value" 375 for i := 0; i < 10000; i++ { 376 largeProps.Fields[k+strconv.Itoa(i)] = &structpb.Value{ 377 Kind: &structpb.Value_StringValue{ 378 StringValue: v, 379 }, 380 } 381 } 382 outProp := &BuildOutputProperties{ 383 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 384 Proto: largeProps, 385 } 386 So(outProp.Put(ctx), ShouldBeNil) 387 So(outProp.Proto, ShouldResembleProto, largeProps) 388 So(outProp.ChunkCount, ShouldAlmostEqual, 1, 1) 389 390 count, err := datastore.Count(ctx, datastore.NewQuery("PropertyChunk")) 391 So(err, ShouldBeNil) 392 So(count, ShouldAlmostEqual, 1, 1) 393 394 outPropInDB := &BuildOutputProperties{ 395 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 396 } 397 So(datastore.Get(ctx, outPropInDB), ShouldBeNil) 398 So(outPropInDB.ChunkCount, ShouldAlmostEqual, 1, 1) 399 400 So(outPropInDB.Get(ctx), ShouldBeNil) 401 So(outPropInDB.Proto, ShouldResembleProto, largeProps) 402 So(outPropInDB.ChunkCount, ShouldEqual, 0) 403 404 Convey("large -> small", func() { 405 prop, err := structpb.NewStruct(map[string]any{"key": "value"}) 406 So(err, ShouldBeNil) 407 // Proto got updated to a smaller one. 408 outProp.Proto = prop 409 410 So(outProp.Put(ctx), ShouldBeNil) 411 So(outProp.ChunkCount, ShouldEqual, 0) 412 413 outPropInDB := &BuildOutputProperties{ 414 Build: outProp.Build, 415 } 416 So(outPropInDB.Get(ctx), ShouldBeNil) 417 So(outPropInDB.Proto, ShouldResembleProto, prop) 418 So(outPropInDB.ChunkCount, ShouldEqual, 0) 419 }) 420 421 Convey("large -> larger", func() { 422 larger := proto.Clone(largeProps).(*structpb.Struct) 423 curLen := len(larger.Fields) 424 for i := 0; i < 10; i++ { 425 larger.Fields[k+strconv.Itoa(curLen+i)] = &structpb.Value{ 426 Kind: &structpb.Value_StringValue{ 427 StringValue: v, 428 }, 429 } 430 } 431 // Proto got updated to an even larger one. 432 outProp.Proto = larger 433 434 So(outProp.Put(ctx), ShouldBeNil) 435 So(outProp.ChunkCount, ShouldAlmostEqual, 1, 1) 436 So(outProp.Proto, ShouldResembleProto, larger) 437 438 outPropInDB := &BuildOutputProperties{ 439 Build: outProp.Build, 440 } 441 So(outPropInDB.Get(ctx), ShouldBeNil) 442 So(outPropInDB.Proto, ShouldResembleProto, larger) 443 So(outPropInDB.ChunkCount, ShouldEqual, 0) 444 }) 445 }) 446 447 Convey("too large (>1 chunks)", func() { 448 originMaxPropertySize := maxPropertySize 449 defer func() { 450 maxPropertySize = originMaxPropertySize 451 }() 452 // to avoid take up too much memory in testing. 453 maxPropertySize = 100 454 455 largeProps, err := structpb.NewStruct(map[string]any{}) 456 So(err, ShouldBeNil) 457 k := "largeeeeeee_key" 458 v := "largeeeeeee_value" 459 460 Convey(">1 and <4 chunks", func() { 461 for i := 0; i < 60; i++ { 462 largeProps.Fields[k+strconv.Itoa(i)] = &structpb.Value{ 463 Kind: &structpb.Value_StringValue{ 464 StringValue: v, 465 }, 466 } 467 } 468 outProp := &BuildOutputProperties{ 469 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 470 Proto: largeProps, 471 } 472 So(outProp.Put(ctx), ShouldBeNil) 473 So(outProp.Proto, ShouldResembleProto, largeProps) 474 So(outProp.ChunkCount, ShouldBeBetween, 1, 4) 475 476 count, err := datastore.Count(ctx, datastore.NewQuery("PropertyChunk")) 477 So(err, ShouldBeNil) 478 So(count, ShouldBeBetween, 1, 4) 479 480 outPropInDB := &BuildOutputProperties{ 481 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 482 } 483 So(outPropInDB.Get(ctx), ShouldBeNil) 484 So(outPropInDB.Proto, ShouldResembleProto, largeProps) 485 So(outPropInDB.ChunkCount, ShouldEqual, 0) 486 }) 487 488 Convey("~4 chunks", func() { 489 for i := 0; i < 120; i++ { 490 largeProps.Fields[k+strconv.Itoa(i)] = &structpb.Value{ 491 Kind: &structpb.Value_StringValue{ 492 StringValue: v, 493 }, 494 } 495 } 496 outProp := &BuildOutputProperties{ 497 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 498 Proto: largeProps, 499 } 500 So(outProp.Put(ctx), ShouldBeNil) 501 So(outProp.Proto, ShouldResembleProto, largeProps) 502 So(outProp.ChunkCount, ShouldAlmostEqual, 4, 2) 503 504 count, err := datastore.Count(ctx, datastore.NewQuery("PropertyChunk")) 505 So(err, ShouldBeNil) 506 So(count, ShouldAlmostEqual, 4, 2) 507 508 outPropInDB := &BuildOutputProperties{ 509 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 510 } 511 So(outPropInDB.Get(ctx), ShouldBeNil) 512 So(outPropInDB.Proto, ShouldResembleProto, largeProps) 513 So(outPropInDB.ChunkCount, ShouldEqual, 0) 514 }) 515 516 Convey("> 4 chunks", func() { 517 for i := 0; i < 500; i++ { 518 largeProps.Fields[k+strconv.Itoa(i)] = &structpb.Value{ 519 Kind: &structpb.Value_StringValue{ 520 StringValue: v, 521 }, 522 } 523 } 524 outProp := &BuildOutputProperties{ 525 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 526 Proto: largeProps, 527 } 528 So(outProp.Put(ctx), ShouldBeNil) 529 So(outProp.Proto, ShouldResembleProto, largeProps) 530 So(outProp.ChunkCount, ShouldBeGreaterThan, 4) 531 532 count, err := datastore.Count(ctx, datastore.NewQuery("PropertyChunk")) 533 So(err, ShouldBeNil) 534 So(count, ShouldBeGreaterThan, 4) 535 536 outPropInDB := &BuildOutputProperties{ 537 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 538 } 539 So(outPropInDB.Get(ctx), ShouldBeNil) 540 So(outPropInDB.Proto, ShouldResembleProto, largeProps) 541 So(outPropInDB.ChunkCount, ShouldEqual, 0) 542 543 Convey("missing 2nd Chunk", func() { 544 // Originally, it has >4 chunks. Now, intentionally delete the 2nd chunk 545 chunk2 := &PropertyChunk{ 546 ID: 2, 547 Bytes: []byte("I am not valid compressed bytes."), 548 Parent: datastore.KeyForObj(ctx, &BuildOutputProperties{ 549 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 550 }), 551 } 552 So(datastore.Put(ctx, chunk2), ShouldBeNil) 553 554 outPropInDB := &BuildOutputProperties{ 555 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 556 } 557 err = outPropInDB.Get(ctx) 558 So(err, ShouldErrLike, "failed to decompress output properties bytes") 559 }) 560 561 Convey("missing 5nd Chunk", func() { 562 // Originally, it has >4 chunks. Now, intentionally delete the 5nd chunk 563 chunk5 := &PropertyChunk{ 564 ID: 5, 565 Parent: datastore.KeyForObj(ctx, &BuildOutputProperties{ 566 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 567 }), 568 } 569 So(datastore.Delete(ctx, chunk5), ShouldBeNil) 570 571 outPropInDB := &BuildOutputProperties{ 572 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 573 } 574 err = outPropInDB.Get(ctx) 575 So(err, ShouldErrLike, "failed to fetch the rest chunks for BuildOutputProperties: datastore: no such entity") 576 }) 577 }) 578 }) 579 580 Convey("BuildOutputProperties not exist", func() { 581 outProp := &BuildOutputProperties{ 582 Build: datastore.KeyForObj(ctx, &Build{ 583 ID: 999, 584 }), 585 } 586 So(outProp.Get(ctx), ShouldEqual, datastore.ErrNoSuchEntity) 587 }) 588 589 Convey("GetMultiOutputProperties", func() { 590 largeProps, err := structpb.NewStruct(map[string]any{}) 591 So(err, ShouldBeNil) 592 k := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_key" 593 v := "laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaarge_value" 594 for i := 0; i < 10000; i++ { 595 largeProps.Fields[k+strconv.Itoa(i)] = &structpb.Value{ 596 Kind: &structpb.Value_StringValue{ 597 StringValue: v, 598 }, 599 } 600 } 601 outProp1 := &BuildOutputProperties{ 602 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 603 Proto: largeProps, 604 } 605 outProp2 := &BuildOutputProperties{ 606 Build: datastore.KeyForObj(ctx, &Build{ID: 456}), 607 Proto: largeProps, 608 } 609 So(outProp1.Put(ctx), ShouldBeNil) 610 So(outProp2.Put(ctx), ShouldBeNil) 611 612 outPropInDB1 := &BuildOutputProperties{ 613 Build: datastore.KeyForObj(ctx, &Build{ID: 123}), 614 } 615 outPropInDB2 := &BuildOutputProperties{ 616 Build: datastore.KeyForObj(ctx, &Build{ID: 456}), 617 } 618 So(GetMultiOutputProperties(ctx, outPropInDB1, outPropInDB2), ShouldBeNil) 619 So(outPropInDB1.Proto, ShouldResembleProto, largeProps) 620 So(outPropInDB2.Proto, ShouldResembleProto, largeProps) 621 622 Convey("one empty, one found", func() { 623 outPropInDB1 := &BuildOutputProperties{} 624 outPropInDB2 := &BuildOutputProperties{ 625 Build: datastore.KeyForObj(ctx, &Build{ID: 456}), 626 } 627 So(GetMultiOutputProperties(ctx, outPropInDB1, outPropInDB2), ShouldBeNil) 628 So(outPropInDB1.Proto, ShouldBeNil) 629 So(outPropInDB2.Proto, ShouldResembleProto, largeProps) 630 }) 631 632 Convey("one not found, one found", func() { 633 outPropInDB1 := &BuildOutputProperties{ 634 Build: datastore.KeyForObj(ctx, &Build{ID: 999}), 635 } 636 outPropInDB2 := &BuildOutputProperties{ 637 Build: datastore.KeyForObj(ctx, &Build{ID: 456}), 638 } 639 err := GetMultiOutputProperties(ctx, outPropInDB1, outPropInDB2) 640 So(err, ShouldNotBeNil) 641 me, _ := err.(errors.MultiError) 642 So(me[0], ShouldErrLike, datastore.ErrNoSuchEntity) 643 So(outPropInDB1.Proto, ShouldBeNil) 644 So(outPropInDB2.Proto, ShouldResembleProto, largeProps) 645 }) 646 }) 647 }) 648 }