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  }