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 := &timestamppb.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  }