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  }