go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/tasks/update_build_task_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  	"bytes"
    19  	"context"
    20  	"encoding/base64"
    21  	"encoding/json"
    22  	"io"
    23  	"strconv"
    24  	"testing"
    25  
    26  	"google.golang.org/api/pubsub/v1"
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/types/known/structpb"
    29  
    30  	"go.chromium.org/luci/common/clock/testclock"
    31  	"go.chromium.org/luci/gae/filter/txndefer"
    32  	"go.chromium.org/luci/gae/impl/memory"
    33  	"go.chromium.org/luci/gae/service/datastore"
    34  	"go.chromium.org/luci/server/caching"
    35  	"go.chromium.org/luci/server/caching/cachingtest"
    36  	"go.chromium.org/luci/server/tq"
    37  
    38  	"go.chromium.org/luci/buildbucket/appengine/internal/config"
    39  	"go.chromium.org/luci/buildbucket/appengine/internal/metrics"
    40  	"go.chromium.org/luci/buildbucket/appengine/model"
    41  	pb "go.chromium.org/luci/buildbucket/proto"
    42  
    43  	. "github.com/smartystreets/goconvey/convey"
    44  
    45  	. "go.chromium.org/luci/common/testing/assertions"
    46  )
    47  
    48  func TestValidateBuildTask(t *testing.T) {
    49  	t.Parallel()
    50  
    51  	Convey("ValidateBuildTask", t, func() {
    52  		ctx := memory.Use(context.Background())
    53  
    54  		t0 := testclock.TestRecentTimeUTC
    55  		build := &model.Build{
    56  			ID: 1,
    57  			Proto: &pb.Build{
    58  				Id: 1,
    59  				Builder: &pb.BuilderID{
    60  					Project: "project",
    61  					Bucket:  "bucket",
    62  					Builder: "builder",
    63  				},
    64  				Status: pb.Status_STARTED,
    65  			},
    66  			CreateTime: t0,
    67  		}
    68  		bk := datastore.KeyForObj(ctx, build)
    69  		infra := &model.BuildInfra{
    70  			Build: bk,
    71  			Proto: &pb.BuildInfra{},
    72  		}
    73  		So(datastore.Put(ctx, build, infra), ShouldBeNil)
    74  		Convey("backend not in build infra", func() {
    75  			req := &pb.BuildTaskUpdate{
    76  				BuildId: "1",
    77  				Task: &pb.Task{
    78  					Id: &pb.TaskID{
    79  						Id:     "1",
    80  						Target: "swarming",
    81  					},
    82  				},
    83  			}
    84  			err := validateBuildTask(ctx, req, infra)
    85  			So(err, ShouldErrLike, "Build 1 does not support task backend")
    86  		})
    87  		Convey("task not in build infra", func() {
    88  			infra := &model.BuildInfra{
    89  				Build: bk,
    90  				Proto: &pb.BuildInfra{
    91  					Backend: &pb.BuildInfra_Backend{},
    92  				},
    93  			}
    94  			So(datastore.Put(ctx, infra), ShouldBeNil)
    95  			req := &pb.BuildTaskUpdate{
    96  				BuildId: "1",
    97  				Task: &pb.Task{
    98  					Id: &pb.TaskID{
    99  						Id:     "2",
   100  						Target: "other",
   101  					},
   102  				},
   103  			}
   104  			err := validateBuildTask(ctx, req, infra)
   105  			So(err, ShouldErrLike, "No task is associated with the build. Cannot update.")
   106  		})
   107  		Convey("task ID target mismatch", func() {
   108  			infra := &model.BuildInfra{
   109  				Build: bk,
   110  				Proto: &pb.BuildInfra{
   111  					Backend: &pb.BuildInfra_Backend{
   112  						Task: &pb.Task{
   113  							Id: &pb.TaskID{
   114  								Id:     "2",
   115  								Target: "other",
   116  							},
   117  						},
   118  					},
   119  				},
   120  			}
   121  			So(datastore.Put(ctx, infra), ShouldBeNil)
   122  			req := &pb.BuildTaskUpdate{
   123  				BuildId: "1",
   124  				Task: &pb.Task{
   125  					Id: &pb.TaskID{
   126  						Id:     "1",
   127  						Target: "other",
   128  					},
   129  				},
   130  			}
   131  			err := validateBuildTask(ctx, req, infra)
   132  			So(err, ShouldBeError)
   133  		})
   134  		Convey("Run task did not return yet", func() {
   135  			infra := &model.BuildInfra{
   136  				Build: bk,
   137  				Proto: &pb.BuildInfra{
   138  					Backend: &pb.BuildInfra_Backend{
   139  						Task: &pb.Task{
   140  							Id: &pb.TaskID{
   141  								Target: "swarming",
   142  							},
   143  						},
   144  					},
   145  				},
   146  			}
   147  			So(datastore.Put(ctx, infra), ShouldBeNil)
   148  			req := &pb.BuildTaskUpdate{
   149  				BuildId: "1",
   150  				Task: &pb.Task{
   151  					Id: &pb.TaskID{
   152  						Id:     "1",
   153  						Target: "swarming",
   154  					},
   155  					Status: pb.Status_CANCELED,
   156  				},
   157  			}
   158  			err := validateBuildTask(ctx, req, infra)
   159  			So(err, ShouldErrLike, "No task is associated with the build. Cannot update.")
   160  		})
   161  		Convey("task is complete and success", func() {
   162  			infra := &model.BuildInfra{
   163  				Build: bk,
   164  				Proto: &pb.BuildInfra{
   165  					Backend: &pb.BuildInfra_Backend{
   166  						Task: &pb.Task{
   167  							Status: pb.Status_SUCCESS,
   168  							Id: &pb.TaskID{
   169  								Id:     "1",
   170  								Target: "swarming",
   171  							},
   172  						},
   173  					},
   174  				},
   175  			}
   176  			So(datastore.Put(ctx, infra), ShouldBeNil)
   177  			req := &pb.BuildTaskUpdate{
   178  				BuildId: "1",
   179  				Task: &pb.Task{
   180  					Id: &pb.TaskID{
   181  						Id:     "1",
   182  						Target: "swarming",
   183  					},
   184  				},
   185  			}
   186  			err := validateBuildTask(ctx, req, infra)
   187  			So(err, ShouldBeError)
   188  		})
   189  		Convey("task is cancelled", func() {
   190  			infra := &model.BuildInfra{
   191  				Build: bk,
   192  				Proto: &pb.BuildInfra{
   193  					Backend: &pb.BuildInfra_Backend{
   194  						Task: &pb.Task{
   195  							Status: pb.Status_CANCELED,
   196  							Id: &pb.TaskID{
   197  								Id:     "1",
   198  								Target: "swarming",
   199  							},
   200  						},
   201  					},
   202  				},
   203  			}
   204  			So(datastore.Put(ctx, infra), ShouldBeNil)
   205  			req := &pb.BuildTaskUpdate{
   206  				BuildId: "1",
   207  				Task: &pb.Task{
   208  					Id: &pb.TaskID{
   209  						Id:     "1",
   210  						Target: "swarming",
   211  					},
   212  				},
   213  			}
   214  			err := validateBuildTask(ctx, req, infra)
   215  			So(err, ShouldBeError)
   216  		})
   217  		Convey("task is running", func() {
   218  			infra := &model.BuildInfra{
   219  				Build: bk,
   220  				Proto: &pb.BuildInfra{
   221  					Backend: &pb.BuildInfra_Backend{
   222  						Task: &pb.Task{
   223  							Status: pb.Status_STARTED,
   224  							Id: &pb.TaskID{
   225  								Id:     "1",
   226  								Target: "swarming",
   227  							},
   228  						},
   229  					},
   230  				},
   231  			}
   232  			So(datastore.Put(ctx, infra), ShouldBeNil)
   233  			req := &pb.BuildTaskUpdate{
   234  				BuildId: "1",
   235  				Task: &pb.Task{
   236  					Id: &pb.TaskID{
   237  						Id:     "1",
   238  						Target: "swarming",
   239  					},
   240  				},
   241  			}
   242  			err := validateBuildTask(ctx, req, infra)
   243  			So(err, ShouldBeNil)
   244  		})
   245  
   246  	})
   247  }
   248  
   249  func TestValidateTaskUpdate(t *testing.T) {
   250  	t.Parallel()
   251  
   252  	Convey("ValidateTaskUpdate", t, func() {
   253  		ctx := memory.Use(context.Background())
   254  
   255  		Convey("is valid task", func() {
   256  			req := &pb.BuildTaskUpdate{
   257  				BuildId: "1",
   258  				Task: &pb.Task{
   259  					Status: pb.Status_STARTED,
   260  					Id: &pb.TaskID{
   261  						Id:     "one",
   262  						Target: "swarming",
   263  					},
   264  					UpdateId: 1,
   265  				},
   266  			}
   267  			So(validateBuildTaskUpdate(ctx, req), ShouldBeNil)
   268  		})
   269  		Convey("is missing task", func() {
   270  			req := &pb.BuildTaskUpdate{
   271  				BuildId: "1",
   272  			}
   273  			err := validateBuildTaskUpdate(ctx, req)
   274  			So(err, ShouldNotBeNil)
   275  			So(err.Error(), ShouldContainSubstring, "task.id: required")
   276  		})
   277  		Convey("is missing build ID", func() {
   278  			req := &pb.BuildTaskUpdate{
   279  				Task: &pb.Task{
   280  					Status: pb.Status_STARTED,
   281  					Id: &pb.TaskID{
   282  						Id:     "one",
   283  						Target: "swarming",
   284  					},
   285  				},
   286  			}
   287  			err := validateBuildTaskUpdate(ctx, req)
   288  			So(err, ShouldNotBeNil)
   289  			So(err.Error(), ShouldContainSubstring, "build_id required")
   290  		})
   291  		Convey("is missing task ID", func() {
   292  			req := &pb.BuildTaskUpdate{
   293  				BuildId: "1",
   294  				Task: &pb.Task{
   295  					Status: pb.Status_STARTED,
   296  				},
   297  			}
   298  			err := validateBuildTaskUpdate(ctx, req)
   299  			So(err, ShouldNotBeNil)
   300  			So(err.Error(), ShouldContainSubstring, "task.id: required")
   301  		})
   302  		Convey("is missing update ID", func() {
   303  			req := &pb.BuildTaskUpdate{
   304  				BuildId: "1",
   305  				Task: &pb.Task{
   306  					Status: pb.Status_STARTED,
   307  					Id: &pb.TaskID{
   308  						Id:     "one",
   309  						Target: "swarming",
   310  					},
   311  				},
   312  			}
   313  			err := validateBuildTaskUpdate(ctx, req)
   314  			So(err, ShouldNotBeNil)
   315  			So(err.Error(), ShouldContainSubstring, "task.UpdateId: required")
   316  		})
   317  		Convey("is invalid task status: SCHEDULED", func() {
   318  			req := &pb.BuildTaskUpdate{
   319  				BuildId: "1",
   320  				Task: &pb.Task{
   321  					Status: pb.Status_SCHEDULED,
   322  					Id: &pb.TaskID{
   323  						Id:     "one",
   324  						Target: "swarming",
   325  					},
   326  					UpdateId: 1,
   327  				},
   328  			}
   329  			err := validateBuildTaskUpdate(ctx, req)
   330  			So(err, ShouldNotBeNil)
   331  			So(err.Error(), ShouldContainSubstring, "invalid status SCHEDULED")
   332  		})
   333  		Convey("is invalid task status: ENDED_MASK", func() {
   334  			req := &pb.BuildTaskUpdate{
   335  				BuildId: "1",
   336  				Task: &pb.Task{
   337  					Status: pb.Status_ENDED_MASK,
   338  					Id: &pb.TaskID{
   339  						Id:     "one",
   340  						Target: "swarming",
   341  					},
   342  					UpdateId: 1,
   343  				},
   344  			}
   345  			err := validateBuildTaskUpdate(ctx, req)
   346  			So(err, ShouldNotBeNil)
   347  			So(err.Error(), ShouldContainSubstring, "invalid status ENDED_MASK")
   348  		})
   349  		Convey("is invalid task status: STATUS_UNSPECIFIED", func() {
   350  			req := &pb.BuildTaskUpdate{
   351  				BuildId: "1",
   352  				Task: &pb.Task{
   353  					Status: pb.Status_STATUS_UNSPECIFIED,
   354  					Id: &pb.TaskID{
   355  						Id:     "one",
   356  						Target: "swarming",
   357  					},
   358  					UpdateId: 1,
   359  				},
   360  			}
   361  			err := validateBuildTaskUpdate(ctx, req)
   362  			So(err, ShouldNotBeNil)
   363  			So(err.Error(), ShouldContainSubstring, "invalid status STATUS_UNSPECIFIED")
   364  		})
   365  		Convey("is invalid task detail", func() {
   366  
   367  			details := make(map[string]*structpb.Value)
   368  			for i := 0; i < 10000; i++ {
   369  				v, _ := structpb.NewValue("my really long detail, but it's not that long.")
   370  				details[strconv.Itoa(i)] = v
   371  			}
   372  			req := &pb.BuildTaskUpdate{
   373  				BuildId: "1",
   374  				Task: &pb.Task{
   375  					Status: pb.Status_STARTED,
   376  					Details: &structpb.Struct{
   377  						Fields: details,
   378  					},
   379  					Id: &pb.TaskID{
   380  						Id:     "one",
   381  						Target: "swarming",
   382  					},
   383  					UpdateId: 1,
   384  				},
   385  			}
   386  			err := validateBuildTaskUpdate(ctx, req)
   387  			So(err, ShouldNotBeNil)
   388  			So(err.Error(), ShouldContainSubstring, "task.details is greater than 10 kb")
   389  		})
   390  	})
   391  }
   392  
   393  func TestUpdateTaskEntity(t *testing.T) {
   394  	t.Parallel()
   395  	Convey("UpdateTaskEntity", t, func() {
   396  		ctx, sch := tq.TestingContext(memory.Use(context.Background()), nil)
   397  		ctx = txndefer.FilterRDS(ctx)
   398  		ctx = metrics.WithServiceInfo(ctx, "svc", "job", "ins")
   399  
   400  		t0 := testclock.TestRecentTimeUTC
   401  
   402  		taskProto := &pb.Task{
   403  			Status: pb.Status_SCHEDULED,
   404  			Id: &pb.TaskID{
   405  				Id:     "1",
   406  				Target: "swarming",
   407  			},
   408  			Link:     "a link",
   409  			UpdateId: 50,
   410  		}
   411  		infraProto := &pb.BuildInfra{
   412  			Backend: &pb.BuildInfra_Backend{
   413  				Task: taskProto,
   414  			},
   415  		}
   416  		buildProto := &pb.Build{
   417  			Id: 1,
   418  			Builder: &pb.BuilderID{
   419  				Project: "project",
   420  				Bucket:  "bucket",
   421  				Builder: "builder",
   422  			},
   423  			Status: pb.Status_STARTED,
   424  		}
   425  		buildModel := &model.Build{
   426  			ID:         1,
   427  			Proto:      buildProto,
   428  			CreateTime: t0,
   429  			Status:     pb.Status_STARTED,
   430  		}
   431  		bk := datastore.KeyForObj(ctx, buildModel)
   432  		infraModel := &model.BuildInfra{
   433  			Build: bk,
   434  			Proto: infraProto,
   435  		}
   436  		So(datastore.Put(ctx, buildModel, infraModel), ShouldBeNil)
   437  		Convey("normal task save", func() {
   438  			req := &pb.BuildTaskUpdate{
   439  				BuildId: "1",
   440  				Task: &pb.Task{
   441  					Status: pb.Status_STARTED,
   442  					Id: &pb.TaskID{
   443  						Id:     "1",
   444  						Target: "swarming",
   445  					},
   446  					UpdateId: 100,
   447  				},
   448  			}
   449  			err := updateTaskEntity(ctx, req, 1)
   450  			So(err, ShouldBeNil)
   451  			bk := datastore.KeyForObj(ctx, &model.Build{ID: 1})
   452  			resultInfraModel := &model.BuildInfra{
   453  				Build: bk,
   454  			}
   455  			result := datastore.Get(ctx, resultInfraModel)
   456  			So(result, ShouldBeNil)
   457  			So(resultInfraModel.Proto, ShouldResembleProto, &pb.BuildInfra{
   458  				Backend: &pb.BuildInfra_Backend{
   459  					Task: &pb.Task{
   460  						Status: pb.Status_STARTED,
   461  						Id: &pb.TaskID{
   462  							Id:     "1",
   463  							Target: "swarming",
   464  						},
   465  						Link:     "a link",
   466  						UpdateId: 100,
   467  					},
   468  				},
   469  			})
   470  		})
   471  
   472  		Convey("old update_id", func() {
   473  			req := &pb.BuildTaskUpdate{
   474  				BuildId: "1",
   475  				Task: &pb.Task{
   476  					Status: pb.Status_STARTED,
   477  					Id: &pb.TaskID{
   478  						Id:     "1",
   479  						Target: "swarming",
   480  					},
   481  					Link:     "a link",
   482  					UpdateId: 2,
   483  				},
   484  			}
   485  			err := updateTaskEntity(ctx, req, 1)
   486  			So(err, ShouldBeNil)
   487  		})
   488  
   489  		Convey("end a task", func() {
   490  			bs := &model.BuildStatus{
   491  				Build:  bk,
   492  				Status: pb.Status_STARTED,
   493  			}
   494  			b, err := proto.Marshal(&pb.Build{
   495  				Steps: []*pb.Step{
   496  					{
   497  						Name: "step",
   498  					},
   499  				},
   500  			})
   501  			So(err, ShouldBeNil)
   502  			steps := &model.BuildSteps{
   503  				ID:       1,
   504  				Build:    bk,
   505  				IsZipped: false,
   506  				Bytes:    b,
   507  			}
   508  			So(datastore.Put(ctx, buildModel, infraModel, bs, steps), ShouldBeNil)
   509  
   510  			endReq := &pb.BuildTaskUpdate{
   511  				BuildId: "1",
   512  				Task: &pb.Task{
   513  					Status: pb.Status_INFRA_FAILURE,
   514  					Id: &pb.TaskID{
   515  						Id:     "1",
   516  						Target: "swarming",
   517  					},
   518  					UpdateId: 200,
   519  				},
   520  			}
   521  			err = updateTaskEntity(ctx, endReq, 1)
   522  			So(err, ShouldBeNil)
   523  			resultInfraModel := &model.BuildInfra{
   524  				Build: bk,
   525  			}
   526  			result := datastore.Get(ctx, resultInfraModel, bs, buildModel, steps)
   527  			So(result, ShouldBeNil)
   528  			So(resultInfraModel.Proto, ShouldResembleProto, &pb.BuildInfra{
   529  				Backend: &pb.BuildInfra_Backend{
   530  					Task: &pb.Task{
   531  						Status: pb.Status_INFRA_FAILURE,
   532  						Id: &pb.TaskID{
   533  							Id:     "1",
   534  							Target: "swarming",
   535  						},
   536  						UpdateId: 200,
   537  						Link:     "a link",
   538  					},
   539  				},
   540  			})
   541  
   542  			So(sch.Tasks(), ShouldHaveLength, 4)
   543  			So(bs.Status, ShouldEqual, pb.Status_INFRA_FAILURE)
   544  			So(buildModel.Proto.Status, ShouldEqual, pb.Status_INFRA_FAILURE)
   545  			stp, err := steps.ToProto(ctx)
   546  			So(err, ShouldBeNil)
   547  			for _, s := range stp {
   548  				So(s.Status, ShouldEqual, pb.Status_CANCELED)
   549  			}
   550  		})
   551  		Convey("start a task", func() {
   552  			buildModel.Proto.Status = pb.Status_SCHEDULED
   553  			infraModel.Proto.Backend.Task.Status = pb.Status_SCHEDULED
   554  			bs := &model.BuildStatus{
   555  				Build:  bk,
   556  				Status: pb.Status_SCHEDULED,
   557  			}
   558  			So(datastore.Put(ctx, buildModel, infraModel, bs), ShouldBeNil)
   559  
   560  			endReq := &pb.BuildTaskUpdate{
   561  				BuildId: "1",
   562  				Task: &pb.Task{
   563  					Status: pb.Status_STARTED,
   564  					Id: &pb.TaskID{
   565  						Id:     "1",
   566  						Target: "swarming",
   567  					},
   568  					UpdateId: 200,
   569  				},
   570  			}
   571  			err := updateTaskEntity(ctx, endReq, 1)
   572  			So(err, ShouldBeNil)
   573  			resultInfraModel := &model.BuildInfra{
   574  				Build: bk,
   575  			}
   576  			result := datastore.Get(ctx, resultInfraModel, bs, buildModel)
   577  			So(result, ShouldBeNil)
   578  			So(resultInfraModel.Proto.Backend.Task.Status, ShouldEqual, pb.Status_STARTED)
   579  
   580  			So(sch.Tasks(), ShouldHaveLength, 0)
   581  			So(bs.Status, ShouldEqual, pb.Status_SCHEDULED)
   582  			So(buildModel.Proto.Status, ShouldEqual, pb.Status_SCHEDULED)
   583  		})
   584  
   585  		Convey("RunTask did not return but UpdateBuildTask called", func() {
   586  			buildModel.Proto.Status = pb.Status_SCHEDULED
   587  			infraModel.Proto.Backend.Task.Id.Id = ""
   588  			infraModel.Proto.Backend.Task.UpdateId = 0
   589  			infraModel.Proto.Backend.Task.Link = ""
   590  			bs := &model.BuildStatus{
   591  				Build:  bk,
   592  				Status: pb.Status_SCHEDULED,
   593  			}
   594  			So(datastore.Put(ctx, buildModel, infraModel, bs), ShouldBeNil)
   595  
   596  			endReq := &pb.BuildTaskUpdate{
   597  				BuildId: "1",
   598  				Task: &pb.Task{
   599  					Status: pb.Status_STARTED,
   600  					Id: &pb.TaskID{
   601  						Id:     "1",
   602  						Target: "swarming",
   603  					},
   604  					UpdateId: 200,
   605  				},
   606  			}
   607  			err := updateTaskEntity(ctx, endReq, 1)
   608  			So(err, ShouldBeNil)
   609  			resultInfraModel := &model.BuildInfra{
   610  				Build: bk,
   611  			}
   612  			result := datastore.Get(ctx, resultInfraModel, bs, buildModel)
   613  			So(result, ShouldBeNil)
   614  			So(resultInfraModel.Proto.Backend.Task.Status, ShouldEqual, pb.Status_STARTED)
   615  
   616  			So(sch.Tasks(), ShouldHaveLength, 0)
   617  			So(bs.Status, ShouldEqual, pb.Status_SCHEDULED)
   618  			So(buildModel.Proto.Status, ShouldEqual, pb.Status_SCHEDULED)
   619  
   620  			post_infra := &model.BuildInfra{Build: bk}
   621  			So(datastore.Get(ctx, post_infra), ShouldBeNil)
   622  			So(post_infra.Proto.Backend.Task, ShouldResembleProto, &pb.Task{
   623  				Status: pb.Status_STARTED,
   624  				Id: &pb.TaskID{
   625  					Id:     "1",
   626  					Target: "swarming",
   627  				},
   628  				UpdateId: 200,
   629  			})
   630  		})
   631  	})
   632  }
   633  
   634  func TestUpdateBuildTask(t *testing.T) {
   635  	t.Parallel()
   636  
   637  	Convey("pubsub handler", t, func() {
   638  		ctx := memory.UseWithAppID(context.Background(), "dev~app-id")
   639  		ctx = cachingtest.WithGlobalCache(ctx, map[string]caching.BlobCache{
   640  			"update-build-task-pubsub-msg-id": cachingtest.NewBlobCache(),
   641  		})
   642  		So(config.SetTestSettingsCfg(ctx, &pb.SettingsCfg{
   643  			Backends: []*pb.BackendSetting{
   644  				{
   645  					Target:   "swarming://chromium-swarm",
   646  					Hostname: "chromium-swarm.appspot.com",
   647  					Mode: &pb.BackendSetting_FullMode_{
   648  						FullMode: &pb.BackendSetting_FullMode{
   649  							PubsubId: "chromium-swarm-backend",
   650  						},
   651  					},
   652  				},
   653  				{
   654  					Target:   "foo://foo-backend",
   655  					Hostname: "foo.com",
   656  					Mode:     &pb.BackendSetting_LiteMode_{},
   657  				},
   658  			},
   659  		}), ShouldBeNil)
   660  
   661  		t0 := testclock.TestRecentTimeUTC
   662  
   663  		// Create a "scheduled" build.
   664  		build := &model.Build{
   665  			ID: 1,
   666  			Proto: &pb.Build{
   667  				Id: 1,
   668  				Builder: &pb.BuilderID{
   669  					Project: "project",
   670  					Bucket:  "bucket",
   671  					Builder: "builder",
   672  				},
   673  				Status: pb.Status_SCHEDULED,
   674  			},
   675  			CreateTime: t0,
   676  		}
   677  		bk := datastore.KeyForObj(ctx, build)
   678  		infra := &model.BuildInfra{
   679  			Build: bk,
   680  			Proto: &pb.BuildInfra{
   681  				Buildbucket: &pb.BuildInfra_Buildbucket{
   682  					Hostname: "bbhost",
   683  					Agent: &pb.BuildInfra_Buildbucket_Agent{
   684  						Input: &pb.BuildInfra_Buildbucket_Agent_Input{
   685  							Data: map[string]*pb.InputDataRef{},
   686  						},
   687  					},
   688  				},
   689  				Backend: &pb.BuildInfra_Backend{
   690  					Task: &pb.Task{
   691  						Id: &pb.TaskID{
   692  							Target: "swarming://chromium-swarm",
   693  						},
   694  						UpdateId: 0,
   695  					},
   696  				},
   697  			},
   698  		}
   699  		Convey("full mode", func() {
   700  			Convey("ok", func() {
   701  				// Update the backend task as if RunTask had responded.
   702  				infra.Proto.Backend.Task.Id.Id = "one"
   703  				infra.Proto.Backend.Task.UpdateId = 1
   704  				infra.Proto.Backend.Task.Status = pb.Status_SCHEDULED
   705  				So(datastore.Put(ctx, build, infra), ShouldBeNil)
   706  				req := &pb.BuildTaskUpdate{
   707  					BuildId: "1",
   708  					Task: &pb.Task{
   709  						Status: pb.Status_STARTED,
   710  						Id: &pb.TaskID{
   711  							Id:     "one",
   712  							Target: "swarming://chromium-swarm",
   713  						},
   714  						UpdateId:        2,
   715  						SummaryMarkdown: "imo, html is ugly to read",
   716  					},
   717  				}
   718  				body := makeUpdateBuildTaskPubsubMsg(req, "msg_id_1", "chromium-swarm-backend")
   719  				So(UpdateBuildTask(ctx, body), ShouldBeRPCOK)
   720  
   721  				expectedBuildInfra := &model.BuildInfra{Build: datastore.KeyForObj(ctx, &model.Build{ID: 1})}
   722  				So(datastore.Get(ctx, expectedBuildInfra), ShouldBeNil)
   723  				So(expectedBuildInfra.Proto.Backend.Task.Status, ShouldEqual, pb.Status_STARTED)
   724  				So(expectedBuildInfra.Proto.Backend.Task.UpdateId, ShouldEqual, 2)
   725  				So(expectedBuildInfra.Proto.Backend.Task.SummaryMarkdown, ShouldEqual, "imo, html is ugly to read")
   726  			})
   727  
   728  			Convey("task is not registered", func() {
   729  				So(datastore.Put(ctx, build, infra), ShouldBeNil)
   730  				req := &pb.BuildTaskUpdate{
   731  					BuildId: "1",
   732  					Task: &pb.Task{
   733  						Status: pb.Status_STARTED,
   734  						Id: &pb.TaskID{
   735  							Id:     "one",
   736  							Target: "swarming://chromium-swarm",
   737  						},
   738  						UpdateId:        2,
   739  						SummaryMarkdown: "imo, html is ugly to read",
   740  					},
   741  				}
   742  				body := makeUpdateBuildTaskPubsubMsg(req, "msg_id_1", "chromium-swarm-backend")
   743  				So(UpdateBuildTask(ctx, body), ShouldErrLike, "No task is associated with the build. Cannot update.")
   744  			})
   745  
   746  			Convey("subscription id mismatch", func() {
   747  				req := &pb.BuildTaskUpdate{
   748  					BuildId: "1",
   749  					Task: &pb.Task{
   750  						Status: pb.Status_STARTED,
   751  						Id: &pb.TaskID{
   752  							Id:     "one",
   753  							Target: "swarming://chromium-swarm",
   754  						},
   755  						UpdateId:        2,
   756  						SummaryMarkdown: "imo, html is ugly to read",
   757  					},
   758  				}
   759  				body := makeUpdateBuildTaskPubsubMsg(req, "msg_id_1", "chromium-swarm-backend-v2")
   760  				So(UpdateBuildTask(ctx, body), ShouldErrLike, "pubsub subscription projects/app-id/subscriptions/chromium-swarm-backend-v2 did not match the one configured for target swarming://chromium-swarm")
   761  			})
   762  		})
   763  		Convey("lite mode", func() {
   764  			req := &pb.BuildTaskUpdate{
   765  				BuildId: "1",
   766  				Task: &pb.Task{
   767  					Status: pb.Status_STARTED,
   768  					Id: &pb.TaskID{
   769  						Id:     "one",
   770  						Target: "foo://foo-backend",
   771  					},
   772  					UpdateId: 2,
   773  				},
   774  			}
   775  			body := makeUpdateBuildTaskPubsubMsg(req, "msg_id_1", "foo")
   776  			So(UpdateBuildTask(ctx, body), ShouldErrLike, "backend target foo://foo-backend is in lite mode. The task update isn't supported")
   777  		})
   778  	})
   779  }
   780  
   781  func makeUpdateBuildTaskPubsubMsg(req *pb.BuildTaskUpdate, msgID, subID string) io.Reader {
   782  	data, err := proto.Marshal(req)
   783  	if err != nil {
   784  		return nil
   785  	}
   786  	msg := &pushRequest{
   787  		Message: pubsub.PubsubMessage{
   788  			Data:      base64.StdEncoding.EncodeToString(data),
   789  			MessageId: msgID,
   790  		},
   791  		Subscription: "projects/app-id/subscriptions/" + subID,
   792  	}
   793  	jmsg, _ := json.Marshal(msg)
   794  	return bytes.NewReader(jmsg)
   795  }