go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/buildbucket/fake/construct.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 bbfake 16 17 import ( 18 "fmt" 19 "time" 20 21 "google.golang.org/protobuf/proto" 22 "google.golang.org/protobuf/types/known/structpb" 23 "google.golang.org/protobuf/types/known/timestamppb" 24 25 bbpb "go.chromium.org/luci/buildbucket/proto" 26 "go.chromium.org/luci/buildbucket/protoutil" 27 "go.chromium.org/luci/common/errors" 28 ) 29 30 // BuildConstructor provides fluent APIs to reduce the boilerplate 31 // when constructing test build. 32 type BuildConstructor struct { 33 host string 34 id int64 35 builderID *bbpb.BuilderID 36 status bbpb.Status 37 createTime time.Time 38 startTime time.Time 39 endTime time.Time 40 updateTime time.Time 41 timeout bool 42 summaryMarkdown string 43 gerritChanges []*bbpb.GerritChange 44 experimental bool 45 requestedProperties *structpb.Struct 46 resultdbHost string 47 invocation string 48 49 template *bbpb.Build 50 } 51 52 // NewBuildConstructor creates a new constructor from scratch. 53 func NewBuildConstructor() *BuildConstructor { 54 return &BuildConstructor{} 55 } 56 57 // NewConstructorFromBuild creates a new constructor with initial value 58 // populated based on the provided build. 59 // 60 // Providing nil build is equivalent to `NewBuildConstructor()` 61 func NewConstructorFromBuild(build *bbpb.Build) *BuildConstructor { 62 if build == nil { 63 return NewBuildConstructor() 64 } 65 bc := &BuildConstructor{ 66 host: build.GetInfra().GetBuildbucket().GetHostname(), 67 id: build.GetId(), 68 builderID: proto.Clone(build.GetBuilder()).(*bbpb.BuilderID), 69 status: build.GetStatus(), 70 timeout: build.GetStatusDetails().GetTimeout() != nil, 71 summaryMarkdown: build.GetSummaryMarkdown(), 72 gerritChanges: make([]*bbpb.GerritChange, len(build.GetInput().GetGerritChanges())), 73 experimental: build.GetInput().GetExperimental(), 74 requestedProperties: proto.Clone(build.GetInfra().GetBuildbucket().GetRequestedProperties()).(*structpb.Struct), 75 resultdbHost: build.GetInfra().GetResultdb().GetHostname(), 76 invocation: build.GetInfra().GetResultdb().GetInvocation(), 77 78 template: proto.Clone(build).(*bbpb.Build), 79 } 80 81 if createTime := build.GetCreateTime(); createTime != nil { 82 bc.createTime = createTime.AsTime() 83 } 84 if startTime := build.GetStartTime(); startTime != nil { 85 bc.startTime = startTime.AsTime() 86 } 87 if endTime := build.GetEndTime(); endTime != nil { 88 bc.endTime = endTime.AsTime() 89 } 90 if updateTime := build.GetUpdateTime(); updateTime != nil { 91 bc.updateTime = updateTime.AsTime() 92 } 93 for i, gc := range build.GetInput().GetGerritChanges() { 94 bc.gerritChanges[i] = proto.Clone(gc).(*bbpb.GerritChange) 95 } 96 return bc 97 } 98 99 // WithHost specifies the host of this Build. Required. 100 func (bc *BuildConstructor) WithHost(host string) *BuildConstructor { 101 bc.host = host 102 return bc 103 } 104 105 // WithID specifies the Build ID. Required. 106 func (bc *BuildConstructor) WithID(id int64) *BuildConstructor { 107 bc.id = id 108 return bc 109 } 110 111 // WithBuilderID specifies the Builder. Required. 112 func (bc *BuildConstructor) WithBuilderID(builderID *bbpb.BuilderID) *BuildConstructor { 113 bc.builderID = builderID 114 return bc 115 } 116 117 // WithStatus specifies the Build Status. Required. 118 func (bc *BuildConstructor) WithStatus(status bbpb.Status) *BuildConstructor { 119 bc.status = status 120 return bc 121 } 122 123 // WithCreateTime specifies the create time. Required. 124 func (bc *BuildConstructor) WithCreateTime(createTime time.Time) *BuildConstructor { 125 bc.createTime = createTime.UTC() 126 return bc 127 } 128 129 // WithStartTime specifies the start time. Required if status >= STARTED. 130 func (bc *BuildConstructor) WithStartTime(startTime time.Time) *BuildConstructor { 131 bc.startTime = startTime.UTC() 132 return bc 133 } 134 135 // WithEndTime specifies the end time. Required if status is ended. 136 func (bc *BuildConstructor) WithEndTime(endTime time.Time) *BuildConstructor { 137 bc.endTime = endTime.UTC() 138 return bc 139 } 140 141 // WithUpdateTime specifies the update time. Optional. 142 func (bc *BuildConstructor) WithUpdateTime(updateTime time.Time) *BuildConstructor { 143 bc.updateTime = updateTime.UTC() 144 return bc 145 } 146 147 // WithTimeout sets the timeout bit of this build. Optional. 148 func (bc *BuildConstructor) WithTimeout(isTimeout bool) *BuildConstructor { 149 bc.timeout = isTimeout 150 return bc 151 } 152 153 // WithSummaryMarkdown specifies the summary markdown. Optional 154 func (bc *BuildConstructor) WithSummaryMarkdown(sm string) *BuildConstructor { 155 bc.summaryMarkdown = sm 156 return bc 157 } 158 159 func (bc *BuildConstructor) WithInvocation(resultdbHost, inv string) *BuildConstructor { 160 bc.resultdbHost = resultdbHost 161 bc.invocation = inv 162 return bc 163 } 164 165 // AppendGerritChanges appends Gerrit changes to this build. Optional. 166 func (bc *BuildConstructor) AppendGerritChanges(gcs ...*bbpb.GerritChange) *BuildConstructor { 167 if len(gcs) == 0 { 168 panic("must provide at least one GerritChange") 169 } 170 for _, gc := range gcs { 171 switch { 172 case gc.GetHost() == "": 173 panic(fmt.Errorf("empty gerrit host")) 174 case gc.GetProject() == "": 175 panic(fmt.Errorf("empty gerrit repo")) 176 case gc.GetChange() == 0: 177 panic(fmt.Errorf("zero gerrit CL number")) 178 case gc.GetPatchset() == 0: 179 panic(fmt.Errorf("zero gerrit CL patchset")) 180 } 181 bc.gerritChanges = append(bc.gerritChanges, proto.Clone(gc).(*bbpb.GerritChange)) 182 } 183 return bc 184 } 185 186 // ResetGerritChanges clears all existing Gerrit Changes of this build. 187 func (bc *BuildConstructor) ResetGerritChanges() *BuildConstructor { 188 bc.gerritChanges = nil 189 return bc 190 } 191 192 // WithExperimental marks this build as experimental build. Optional. 193 func (bc *BuildConstructor) WithExperimental(exp bool) *BuildConstructor { 194 bc.experimental = exp 195 return bc 196 } 197 198 // WithRequestedProperties specifies the requested properties. Optional. 199 // 200 // The data will be transformed to proto struct format. 201 func (bc *BuildConstructor) WithRequestedProperties(data map[string]any) *BuildConstructor { 202 var err error 203 bc.requestedProperties, err = structpb.NewStruct(data) 204 if err != nil { 205 panic(errors.Annotate(err, "failed to convert to proto struct").Err()) 206 } 207 return bc 208 } 209 210 // Construct creates a new build based on supplied inputs. 211 func (bc *BuildConstructor) Construct() *bbpb.Build { 212 switch { 213 case bc.host == "": 214 panic(fmt.Errorf("empty host")) 215 case bc.id == 0: 216 panic(fmt.Errorf("zero build ID")) 217 case bc.builderID == nil: 218 panic(fmt.Errorf("empty builder ID")) 219 case bc.status == bbpb.Status_STATUS_UNSPECIFIED: 220 panic(fmt.Errorf("unspecified status")) 221 case bc.createTime.IsZero(): 222 panic(fmt.Errorf("zero create time")) 223 case bc.status >= bbpb.Status_STARTED && bc.startTime.IsZero(): 224 panic(fmt.Errorf("zero start time")) 225 case protoutil.IsEnded(bc.status) && bc.endTime.IsZero(): 226 panic(fmt.Errorf("zero end time")) 227 } 228 ret := bc.template 229 if ret == nil { 230 ret = &bbpb.Build{} 231 } 232 ret.Id = bc.id 233 ret.Builder = bc.builderID 234 ret.Status = bc.status 235 ret.CreateTime = timestamppb.New(bc.createTime) 236 if !bc.startTime.IsZero() { 237 ret.StartTime = timestamppb.New(bc.startTime) 238 } 239 if !bc.endTime.IsZero() { 240 ret.EndTime = timestamppb.New(bc.endTime) 241 } 242 if !bc.updateTime.IsZero() { 243 ret.UpdateTime = timestamppb.New(bc.updateTime) 244 } 245 if bc.timeout { 246 if ret.GetStatusDetails() == nil { 247 ret.StatusDetails = &bbpb.StatusDetails{} 248 } 249 ret.GetStatusDetails().Timeout = &bbpb.StatusDetails_Timeout{} 250 } 251 ret.SummaryMarkdown = bc.summaryMarkdown 252 // Input 253 if ret.GetInput() == nil { 254 ret.Input = &bbpb.Build_Input{} 255 } 256 ret.Input.GerritChanges = bc.gerritChanges 257 if bc.experimental { 258 ret.Input.Experimental = true 259 ret.Input.Experiments = append(ret.Input.Experiments, "luci.non_production") 260 } 261 // Infra 262 if ret.GetInfra() == nil { 263 ret.Infra = &bbpb.BuildInfra{} 264 } 265 if ret.GetInfra().GetBuildbucket() == nil { 266 ret.Infra.Buildbucket = &bbpb.BuildInfra_Buildbucket{} 267 } 268 ret.Infra.Buildbucket.Hostname = bc.host 269 ret.Infra.Buildbucket.RequestedProperties = bc.requestedProperties 270 271 if ret.GetInfra().GetResultdb() == nil { 272 ret.Infra.Resultdb = &bbpb.BuildInfra_ResultDB{} 273 } 274 ret.Infra.Resultdb.Hostname = bc.resultdbHost 275 ret.Infra.Resultdb.Invocation = bc.invocation 276 return ret 277 }